Add weekend open skip, expiry force-close, and Chinese trade labels.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,8 +12,8 @@ from .session import get_session
|
||||
from ..models.db import get_db
|
||||
from ..sim.ledger import Ledger
|
||||
from ..sim.matcher import Matcher
|
||||
from .clock import window_key
|
||||
from .exits import check_exits, resolve_exit_target
|
||||
from .clock import can_open_new, window_key
|
||||
from .exits import check_expiry_close, check_exits, resolve_exit_target
|
||||
from .group import next_group_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,6 +46,7 @@ class StrategyEngine:
|
||||
initial_premium=float(upl.get("initial_premium") or 0),
|
||||
)
|
||||
rest_sec = self.ledger.get_setting_int("rest_seconds", s.rest_seconds)
|
||||
skip_weekends = self.ledger.get_setting_bool("skip_weekends", s.skip_weekends)
|
||||
leverage = self.ledger.get_setting_float("leverage", s.leverage)
|
||||
min_hours = self.ledger.get_setting_float("min_option_hours", s.min_option_hours)
|
||||
min_opt_lev = self.ledger.get_setting_float(
|
||||
@@ -59,6 +60,7 @@ class StrategyEngine:
|
||||
if last_error and "PriceResult" in str(last_error) and "__dict__" in str(last_error):
|
||||
self._set_state(last_error=None)
|
||||
last_error = None
|
||||
allow_open = can_open_new(skip_weekends=skip_weekends)
|
||||
return {
|
||||
"running": bool(row["running"]),
|
||||
"phase": row["phase"],
|
||||
@@ -67,6 +69,7 @@ class StrategyEngine:
|
||||
"rest_until_ms": rest_until,
|
||||
"rest_left_sec": rest_left,
|
||||
"rest_seconds": rest_sec,
|
||||
"skip_weekends": skip_weekends,
|
||||
"exit_mode": exit_mode,
|
||||
"net_profit_target": net_target,
|
||||
"premium_exit_multiple": prem_mult,
|
||||
@@ -74,7 +77,7 @@ class StrategyEngine:
|
||||
"leverage": leverage,
|
||||
"min_option_hours": min_hours,
|
||||
"min_option_leverage": min_opt_lev,
|
||||
"can_open": True,
|
||||
"can_open": allow_open,
|
||||
"last_error": last_error,
|
||||
"position": upl,
|
||||
"ledger": self.ledger.snapshot(),
|
||||
@@ -97,9 +100,13 @@ class StrategyEngine:
|
||||
|
||||
async def start(self) -> dict[str, Any]:
|
||||
self._set_state(running=1, last_error=None, phase="idle")
|
||||
self.ensure_loop()
|
||||
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")
|
||||
return self.state()
|
||||
|
||||
async def emergency_close(self) -> dict[str, Any]:
|
||||
async with self._lock:
|
||||
@@ -137,12 +144,73 @@ class StrategyEngine:
|
||||
)
|
||||
return len(rows)
|
||||
|
||||
def _position_expiry_ms(self, upl: dict[str, Any]) -> int | None:
|
||||
raw = upl.get("expiry_ms")
|
||||
if raw is not None:
|
||||
try:
|
||||
return int(raw)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
ymd = upl.get("expiry_ymd")
|
||||
if ymd:
|
||||
try:
|
||||
from ..exchange.okx.parse import expiry_ms_from_ymd
|
||||
|
||||
return int(expiry_ms_from_ymd(str(ymd)))
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
async def _close_open_position(
|
||||
self,
|
||||
*,
|
||||
reason: str,
|
||||
bypass_liquidity: bool,
|
||||
pending_close: bool,
|
||||
) -> None:
|
||||
if not pending_close:
|
||||
self._set_state(phase="closing", last_error=None)
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_group,
|
||||
reason=reason,
|
||||
bypass_liquidity=bypass_liquidity,
|
||||
)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
elif r.liquidity_wait and not bypass_liquidity:
|
||||
self._set_state(phase="liquidity_wait", last_error=r.detail)
|
||||
else:
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
|
||||
async def _maybe_expiry_close(self) -> bool:
|
||||
"""若持仓已到期则强制全平。返回是否触发到期平仓。"""
|
||||
pos = self.matcher.current_position()
|
||||
if pos.get("status") != "open":
|
||||
return False
|
||||
upl = self.matcher.unrealized()
|
||||
expired = check_expiry_close(expiry_ms=self._position_expiry_ms(upl))
|
||||
if not expired.should_close:
|
||||
return False
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
pending = st["phase"] in ("liquidity_wait", "closing")
|
||||
await self._close_open_position(
|
||||
reason="expiry",
|
||||
bypass_liquidity=True,
|
||||
pending_close=pending,
|
||||
)
|
||||
return True
|
||||
|
||||
async def _loop(self) -> None:
|
||||
logger.info("strategy engine loop started")
|
||||
while True:
|
||||
try:
|
||||
row = self.db.fetchone("SELECT running FROM strategy_state WHERE id=1")
|
||||
if not row or not int(row["running"]):
|
||||
running = bool(row and int(row["running"]))
|
||||
if not running:
|
||||
# 暂停时仍执行到期全平,避免拖过期
|
||||
async with self._lock:
|
||||
await self._maybe_expiry_close()
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
async with self._lock:
|
||||
@@ -180,6 +248,7 @@ class StrategyEngine:
|
||||
# 有未平仓:只盯平仓,绝不开下一组
|
||||
if pos.get("status") == "open":
|
||||
upl = self.matcher.unrealized()
|
||||
expired = check_expiry_close(expiry_ms=self._position_expiry_ms(upl))
|
||||
decision = check_exits(
|
||||
net_pnl=float(upl.get("net_pnl") or 0),
|
||||
exit_mode=exit_mode,
|
||||
@@ -188,19 +257,18 @@ class StrategyEngine:
|
||||
initial_premium=float(upl.get("initial_premium") or 0),
|
||||
)
|
||||
pending_close = st["phase"] in ("liquidity_wait", "closing")
|
||||
if decision.should_close or pending_close:
|
||||
reason = decision.reason or "liquidity_retry"
|
||||
if not pending_close:
|
||||
self._set_state(phase="closing", last_error=None)
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_group, reason=reason, bypass_liquidity=False
|
||||
)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
elif r.liquidity_wait:
|
||||
self._set_state(phase="liquidity_wait", last_error=r.detail)
|
||||
if expired.should_close or decision.should_close or pending_close:
|
||||
if expired.should_close:
|
||||
reason = "expiry"
|
||||
bypass = True
|
||||
else:
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
reason = decision.reason or "liquidity_retry"
|
||||
bypass = False
|
||||
await self._close_open_position(
|
||||
reason=reason,
|
||||
bypass_liquidity=bypass,
|
||||
pending_close=pending_close,
|
||||
)
|
||||
else:
|
||||
self._set_state(phase="open", last_error=None)
|
||||
return
|
||||
@@ -214,10 +282,21 @@ class StrategyEngine:
|
||||
assert st is not None
|
||||
if st["phase"] in ("paused",):
|
||||
return
|
||||
|
||||
# 旧「轮次停开」状态:自动恢复为空闲以便继续
|
||||
if st["phase"] in ("stopped", "outside_window"):
|
||||
self._set_state(phase="idle")
|
||||
|
||||
skip_weekends = self.ledger.get_setting_bool("skip_weekends", s.skip_weekends)
|
||||
if not can_open_new(skip_weekends=skip_weekends):
|
||||
self._set_state(
|
||||
phase="weekend_skip",
|
||||
last_error="周六/周日跳过开仓(上海时区);持仓仍可平仓",
|
||||
)
|
||||
return
|
||||
if st["phase"] == "weekend_skip":
|
||||
self._set_state(phase="idle", last_error=None)
|
||||
|
||||
# 双保险:账本仍显示有仓则不开
|
||||
if self.matcher.has_open_position():
|
||||
self._set_state(phase="open", last_error="有未平仓,禁止开下一组")
|
||||
@@ -265,6 +344,6 @@ def get_engine() -> StrategyEngine:
|
||||
return _engine
|
||||
|
||||
|
||||
def set_engine(e: StrategyEngine | None) -> None:
|
||||
def set_engine(engine: StrategyEngine | None) -> None:
|
||||
global _engine
|
||||
_engine = e
|
||||
_engine = engine
|
||||
|
||||
Reference in New Issue
Block a user