Implement dual target-close paths and residual option expiry.
Document and enforce: A full dual-leg close, B perp-only when deep OTM with residual archive that does not block next open, and expiry settlement when target is missed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -88,6 +88,7 @@ class StrategyEngine:
|
||||
"can_open": allow_open,
|
||||
"last_error": last_error,
|
||||
"position": upl,
|
||||
"residuals": self.matcher.list_residual_options(pending_only=True),
|
||||
"ledger": self.ledger.snapshot(),
|
||||
}
|
||||
|
||||
@@ -118,16 +119,24 @@ class StrategyEngine:
|
||||
|
||||
async def emergency_close(self) -> dict[str, Any]:
|
||||
async with self._lock:
|
||||
# 紧急全平:绕过期权流动性/偏差校验
|
||||
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
close_data: dict[str, Any] | None = None
|
||||
detail = "flat"
|
||||
ok = True
|
||||
pos = self.matcher.current_position()
|
||||
if pos.get("status") == "open":
|
||||
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
|
||||
ok = r.ok
|
||||
detail = r.detail
|
||||
close_data = r.data
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
residuals = self.matcher.settle_all_residuals_now()
|
||||
return {
|
||||
"close": {
|
||||
"ok": r.ok,
|
||||
"detail": r.detail,
|
||||
"liquidity_wait": r.liquidity_wait,
|
||||
"data": r.data,
|
||||
"ok": ok,
|
||||
"detail": detail,
|
||||
"data": close_data,
|
||||
"residuals_settled": residuals,
|
||||
},
|
||||
"state": self.state(),
|
||||
}
|
||||
@@ -175,9 +184,27 @@ class StrategyEngine:
|
||||
reason: str,
|
||||
bypass_liquidity: bool,
|
||||
pending_close: bool,
|
||||
abandon_if_deep_otm: bool = False,
|
||||
) -> None:
|
||||
if not pending_close:
|
||||
self._set_state(phase="closing", last_error=None)
|
||||
|
||||
# 目标平仓 B:远虚 → 只平永续,期权归档
|
||||
if abandon_if_deep_otm and reason != "expiry" and self.matcher.option_is_deep_otm():
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_perp_abandon_option,
|
||||
reason="target_perp_only",
|
||||
)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
self._set_state(
|
||||
last_error=None,
|
||||
phase="resting",
|
||||
)
|
||||
else:
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
return
|
||||
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_group,
|
||||
reason=reason,
|
||||
@@ -186,12 +213,25 @@ class StrategyEngine:
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
elif r.liquidity_wait and not bypass_liquidity:
|
||||
# 等待期间若已变成远虚,下一 tick 走归档
|
||||
if self.matcher.option_is_deep_otm():
|
||||
r2 = await asyncio.to_thread(
|
||||
self.matcher.close_perp_abandon_option,
|
||||
reason="target_perp_only",
|
||||
)
|
||||
if r2.ok:
|
||||
self._after_close()
|
||||
return
|
||||
self._set_state(phase="liquidity_wait", last_error=r.detail)
|
||||
else:
|
||||
self._set_state(phase="closing", last_error=r.detail)
|
||||
|
||||
async def _settle_residuals(self) -> None:
|
||||
await asyncio.to_thread(self.matcher.settle_due_residuals)
|
||||
|
||||
async def _maybe_expiry_close(self) -> bool:
|
||||
"""若持仓已到期则强制全平。返回是否触发到期平仓。"""
|
||||
await self._settle_residuals()
|
||||
pos = self.matcher.current_position()
|
||||
if pos.get("status") != "open":
|
||||
return False
|
||||
@@ -206,6 +246,7 @@ class StrategyEngine:
|
||||
reason="expiry",
|
||||
bypass_liquidity=True,
|
||||
pending_close=pending,
|
||||
abandon_if_deep_otm=False,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -242,6 +283,9 @@ class StrategyEngine:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _tick_async(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
|
||||
@@ -260,7 +304,7 @@ class StrategyEngine:
|
||||
)
|
||||
pos = self.matcher.current_position()
|
||||
|
||||
# 有未平仓:只盯平仓,绝不开下一组
|
||||
# 有活跃持仓:只盯当前组平仓;残留期权不在此扫描
|
||||
if pos.get("status") == "open":
|
||||
upl = self.matcher.unrealized()
|
||||
expired = check_expiry_close(expiry_ms=self._position_expiry_ms(upl))
|
||||
@@ -276,13 +320,17 @@ class StrategyEngine:
|
||||
if expired.should_close:
|
||||
reason = "expiry"
|
||||
bypass = True
|
||||
abandon = False
|
||||
else:
|
||||
reason = decision.reason or "liquidity_retry"
|
||||
bypass = False
|
||||
# 目标达标(或流动性等待重试)时:远虚走只平永续
|
||||
abandon = bool(decision.should_close or pending_close)
|
||||
await self._close_open_position(
|
||||
reason=reason,
|
||||
bypass_liquidity=bypass,
|
||||
pending_close=pending_close,
|
||||
abandon_if_deep_otm=abandon,
|
||||
)
|
||||
else:
|
||||
self._set_state(phase="open", last_error=None)
|
||||
|
||||
Reference in New Issue
Block a user