Add WeCom markdown alerts and keep target exits while strategy is paused.
Notify open/close/start/pause/fault with SIM vs LIVE venue labels; configure webhook in Settings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+139
-15
@@ -146,6 +146,12 @@ class StrategyEngine:
|
||||
|
||||
async def pause(self) -> dict[str, Any]:
|
||||
self._set_state(running=0, phase="paused", last_error=None)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_pause()
|
||||
except Exception:
|
||||
logger.exception("wecom notify_pause failed")
|
||||
return self.state()
|
||||
|
||||
async def start(self) -> dict[str, Any]:
|
||||
@@ -155,13 +161,25 @@ class StrategyEngine:
|
||||
ok, reason = live_ready()
|
||||
if not ok:
|
||||
self._set_state(running=0, phase="paused", last_error=reason)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_fault(title="启动失败", detail=reason, dedupe_key="start_fail")
|
||||
except Exception:
|
||||
pass
|
||||
return self.state()
|
||||
self._set_state(running=1, last_error=None, phase="idle")
|
||||
self.ensure_loop()
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_start()
|
||||
except Exception:
|
||||
logger.exception("wecom notify_start failed")
|
||||
return self.state()
|
||||
|
||||
def ensure_loop(self) -> None:
|
||||
"""保证后台循环在跑(即使策略暂停,也要盯到期全平)。"""
|
||||
"""保证后台循环在跑(暂停时仍盯目标/到期,不新开)。"""
|
||||
if self._task is None or self._task.done():
|
||||
self._task = asyncio.create_task(self._loop(), name="strategy-engine")
|
||||
|
||||
@@ -204,6 +222,17 @@ class StrategyEngine:
|
||||
self.enter_rest_after_close()
|
||||
|
||||
residuals = self.matcher.settle_all_residuals_now()
|
||||
try:
|
||||
if ok:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_close(
|
||||
reason="emergency",
|
||||
detail=detail,
|
||||
data=close_data or {},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("wecom emergency notify failed")
|
||||
return {
|
||||
"close": {
|
||||
"ok": ok,
|
||||
@@ -298,6 +327,16 @@ class StrategyEngine:
|
||||
last_error=None,
|
||||
phase="resting",
|
||||
)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_close(
|
||||
reason="target_perp_only",
|
||||
detail=r.detail,
|
||||
data=r.data or {},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("wecom notify_close failed")
|
||||
else:
|
||||
self._note_retry_result(kind, ok=False, detail=r.detail)
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
@@ -311,6 +350,16 @@ class StrategyEngine:
|
||||
if r.ok:
|
||||
self._note_retry_result(kind, ok=True)
|
||||
self._after_close()
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_close(
|
||||
reason=reason,
|
||||
detail=r.detail,
|
||||
data=r.data or {},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("wecom notify_close failed")
|
||||
elif r.liquidity_wait and not bypass_liquidity:
|
||||
# 等待期间若已变成远虚,下一 tick 走归档
|
||||
if self.matcher.option_is_deep_otm():
|
||||
@@ -321,6 +370,16 @@ class StrategyEngine:
|
||||
if r2.ok:
|
||||
self._note_retry_result(kind, ok=True)
|
||||
self._after_close()
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_close(
|
||||
reason="target_perp_only",
|
||||
detail=r2.detail,
|
||||
data=r2.data or {},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("wecom notify_close failed")
|
||||
return
|
||||
self._note_retry_result("liquidity", ok=False, detail=r.detail)
|
||||
self._set_state(phase="liquidity_wait", last_error=r.detail)
|
||||
@@ -360,9 +419,9 @@ class StrategyEngine:
|
||||
row = self.db.fetchone("SELECT running FROM strategy_state WHERE id=1")
|
||||
running = bool(row and int(row["running"]))
|
||||
if not running:
|
||||
# 暂停时仍执行到期全平,避免拖过期
|
||||
# 暂停:不新开仓;仍盯目标平仓 + 到期平仓
|
||||
async with self._lock:
|
||||
await self._maybe_expiry_close()
|
||||
await self._tick_manage_positions()
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
async with self._lock:
|
||||
@@ -382,21 +441,25 @@ class StrategyEngine:
|
||||
continue
|
||||
logger.exception("strategy tick failed")
|
||||
self._set_state(last_error=err)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_fault(
|
||||
title="策略循环异常",
|
||||
detail=err,
|
||||
dedupe_key=f"tick:{err[:80]}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
sleep_for = 1.0 + max(0.0, self._extra_sleep_sec)
|
||||
self._extra_sleep_sec = 0.0
|
||||
await asyncio.sleep(min(sleep_for, 60.0))
|
||||
|
||||
async def _tick_async(self) -> None:
|
||||
# 残留期权到期结算(与活跃组隔离,不挡开仓)
|
||||
async def _tick_manage_positions(self) -> None:
|
||||
"""有仓时的盯盘:残留结算 / 半仓修复 / 目标平 / 到期平。不新开仓。"""
|
||||
await self._settle_residuals()
|
||||
|
||||
s = get_settings()
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
wkey = window_key()
|
||||
if st["window_key"] != wkey:
|
||||
self._set_state(window_key=wkey, phase="idle")
|
||||
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
exit_mode = self.ledger.get_setting_str("exit_mode", s.exit_mode)
|
||||
@@ -409,7 +472,6 @@ class StrategyEngine:
|
||||
pos = self.matcher.current_position()
|
||||
st_pos = str(pos.get("status") or "flat")
|
||||
|
||||
# 实盘半仓修复:禁止新开;失败指数退避,避免每秒砸期权
|
||||
if st_pos == "half_open":
|
||||
allowed, left = self._retry_allowed("half_open")
|
||||
if not allowed:
|
||||
@@ -425,12 +487,21 @@ class StrategyEngine:
|
||||
self._note_retry_result("half_open", ok=True)
|
||||
self._after_close()
|
||||
self._set_state(phase="resting", last_error=None)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_close(
|
||||
reason="emergency",
|
||||
detail=r.detail or "half_open repaired",
|
||||
data=r.data or {},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
self._note_retry_result("half_open", ok=False, detail=r.detail)
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
return
|
||||
|
||||
# 期权已平、永续待平:只续平永续(带退避)
|
||||
if st_pos == "option_closed_perp_pending":
|
||||
await self._close_open_position(
|
||||
reason="perp_pending_retry",
|
||||
@@ -441,7 +512,6 @@ class StrategyEngine:
|
||||
)
|
||||
return
|
||||
|
||||
# 有活跃持仓:只盯当前组平仓;残留期权不在此扫描
|
||||
if st_pos == "open":
|
||||
upl = self.matcher.unrealized()
|
||||
expired = check_expiry_close(expiry_ms=self._position_expiry_ms(upl))
|
||||
@@ -462,7 +532,6 @@ class StrategyEngine:
|
||||
else:
|
||||
reason = decision.reason or "liquidity_retry"
|
||||
bypass = False
|
||||
# 目标达标(或流动性等待重试)时:远虚走只平永续
|
||||
abandon = bool(decision.should_close or pending_close)
|
||||
rkind = "liquidity" if pending_close else "close"
|
||||
await self._close_open_position(
|
||||
@@ -474,6 +543,24 @@ class StrategyEngine:
|
||||
)
|
||||
else:
|
||||
self._set_state(phase="open", last_error=None)
|
||||
|
||||
async def _tick_async(self) -> None:
|
||||
# 先盯仓(平仓路径)
|
||||
await self._tick_manage_positions()
|
||||
|
||||
s = get_settings()
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
wkey = window_key()
|
||||
if st["window_key"] != wkey:
|
||||
self._set_state(window_key=wkey, phase="idle")
|
||||
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
pos = self.matcher.current_position()
|
||||
st_pos = str(pos.get("status") or "flat")
|
||||
# 仍有活跃仓或半仓:本 tick 不再开新仓
|
||||
if st_pos in ("open", "half_open", "option_closed_perp_pending", "opening"):
|
||||
return
|
||||
|
||||
if st["phase"] == "resting" and st["rest_until_ms"]:
|
||||
@@ -514,6 +601,7 @@ class StrategyEngine:
|
||||
return
|
||||
|
||||
self._set_state(phase="opening", last_error=None)
|
||||
wkey = window_key()
|
||||
count = self._count_groups_for_day(wkey)
|
||||
gid = next_group_id(count)
|
||||
option_inst = (
|
||||
@@ -526,6 +614,16 @@ class StrategyEngine:
|
||||
safe, safe_msg = assert_safe_to_open_live(self.matcher)
|
||||
if not safe:
|
||||
self._set_state(phase="idle", last_error=safe_msg)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_fault(
|
||||
title="开仓前对账拒绝",
|
||||
detail=safe_msg,
|
||||
dedupe_key=f"recon:{safe_msg[:80]}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.open_group,
|
||||
@@ -540,8 +638,34 @@ class StrategyEngine:
|
||||
)
|
||||
if r.ok:
|
||||
self._set_state(phase="open", last_error=None)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_open(
|
||||
group_id=gid,
|
||||
detail=r.detail,
|
||||
extra={
|
||||
"bias": pick.bias,
|
||||
"option_side": pick.option_side,
|
||||
"option_inst_id": option_inst,
|
||||
"strike": pick.pair.strike,
|
||||
"expiry_ymd": pick.pair.expiry_ymd,
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("wecom notify_open failed")
|
||||
else:
|
||||
self._set_state(phase="idle", last_error=r.detail)
|
||||
try:
|
||||
from ..notify import wecom
|
||||
|
||||
wecom.notify_fault(
|
||||
title="开仓失败",
|
||||
detail=r.detail,
|
||||
dedupe_key=f"open_fail:{r.detail[:80]}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_engine: StrategyEngine | None = None
|
||||
|
||||
Reference in New Issue
Block a user