Add option-option hedge mode with SIM/LIVE parity.

Mutual hedge_mode, amplitude OTM selection, 1:1 risk sizing, win-leg/full close, dual audits and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 16:01:16 +08:00
parent 15fe2f72dc
commit ec244c63c6
22 changed files with 2640 additions and 83 deletions
+113 -15
View File
@@ -245,6 +245,22 @@ class StrategyEngine:
"option_qty_eth": opt_qty,
"sizing_mode": sizing_mode,
"risk_based": sizing_mode == "risk_based",
"hedge_mode": (
hm
if (
hm := str(
self.ledger.get_setting_str(
"hedge_mode", s.hedge_mode
)
or s.hedge_mode
or "perp_option"
)
.strip()
.lower()
)
in ("perp_option", "option_option")
else "perp_option"
),
"risk_perp_unit": risk_perp_unit,
"risk_option_unit": risk_option_unit,
"risk_exit_unit": risk_exit_unit,
@@ -812,6 +828,10 @@ class StrategyEngine:
)
pending_close = st["phase"] in ("liquidity_wait", "closing")
if expired.should_close or decision.should_close or pending_close:
is_oo = (
str(upl.get("hedge_mode") or "") == "option_option"
or bool(upl.get("option2_inst_id"))
)
if expired.should_close:
reason = "expiry"
bypass = True
@@ -822,11 +842,66 @@ class StrategyEngine:
bypass = False
abandon = bool(decision.should_close or pending_close)
rkind = "liquidity" if pending_close else "close"
if is_oo and decision.should_close and not expired.should_close:
# 期期达标:只平盈利腿,亏损腿残留
close_oo = getattr(
self.matcher, "close_winning_oo_leave_residual", None
)
if close_oo is not None:
r = await asyncio.to_thread(
close_oo, reason="target_oo_win"
)
if r.ok:
self._enter_rest_after_close()
self._set_state(phase="resting", last_error=None)
else:
self._set_state(
phase="liquidity_wait",
last_error=r.detail or "期期盈利腿暂不可平",
)
return
if is_oo and (
expired.should_close
or reason in ("expiry", "emergency", "manual")
or bypass
):
close_full = getattr(self.matcher, "close_oo_full", None)
if close_full is not None and (
expired.should_close or bypass or reason == "emergency"
):
# LIVE:先交易所卖两腿
for sell_fn_name in (
"_live_sell_oo_both",
"live_sell_oo_both",
):
sell_both = getattr(self.matcher, sell_fn_name, None)
if callable(sell_both):
try:
await asyncio.to_thread(
sell_both, bypass_liquidity=bypass
)
except Exception:
logger.exception("live sell oo both failed")
break
r = await asyncio.to_thread(
close_full,
reason=reason if reason != "liquidity_retry" else "expiry",
bypass_liquidity=True,
)
if r.ok:
self._enter_rest_after_close()
self._set_state(phase="resting", last_error=None)
else:
self._set_state(
phase="liquidity_wait",
last_error=r.detail or "期期全平失败",
)
return
await self._close_open_position(
reason=reason,
bypass_liquidity=bypass,
pending_close=pending_close,
abandon_if_deep_otm=abandon,
abandon_if_deep_otm=abandon and not is_oo,
retry_kind=rkind,
)
else:
@@ -948,10 +1023,14 @@ class StrategyEngine:
# 选约后:定仓落库 → 兑 USDC → 资金门 fail-closed(与手动开仓同一管道)
from .open_pipeline import size_and_gate
oo = getattr(pick, "hedge_mode", "perp_option") == "option_option"
prep = size_and_gate(
index_px=float(pick.underlying_px),
option_ask=float(pick.option_ask),
db=self.db,
call_ask=float(pick.call_ask) if oo else None,
put_ask=float(pick.put_ask) if oo else None,
hedge_mode="option_option" if oo else "perp_option",
)
if not prep.ok:
phase = "wait_funds" if prep.capacity is not None else "idle"
@@ -977,9 +1056,6 @@ class StrategyEngine:
wkey = window_key()
count = self._count_groups_for_day(wkey)
gid = next_group_id(count)
option_inst = (
pick.pair.call_inst_id if pick.option_side == "call" else pick.pair.put_inst_id
)
entry_idx = pick.underlying_px
if not get_settings().is_sim:
from ..live.reconcile import assert_safe_to_open_live
@@ -998,17 +1074,39 @@ class StrategyEngine:
except Exception:
pass
return
r = await asyncio.to_thread(
self.matcher.open_group,
group_id=gid,
bias=pick.bias,
option_side=pick.option_side,
perp_side=pick.perp_side,
option_inst_id=option_inst,
entry_index_px=float(entry_idx),
strike=pick.pair.strike,
expiry_ymd=pick.pair.expiry_ymd,
)
if oo:
open_fn = getattr(self.matcher, "open_oo_group", None)
if open_fn is None:
self._set_state(phase="idle", last_error="当前执行器不支持期期开仓")
return
r = await asyncio.to_thread(
open_fn,
group_id=gid,
call_inst_id=str(pick.call_inst_id or pick.pair.call_inst_id),
put_inst_id=str(pick.put_inst_id or pick.pair.put_inst_id),
call_strike=float(pick.call_strike or pick.pair.strike),
put_strike=float(pick.put_strike or pick.pair.strike),
entry_index_px=float(entry_idx),
expiry_ymd=pick.pair.expiry_ymd,
)
option_inst = str(pick.call_inst_id or pick.pair.call_inst_id)
else:
option_inst = (
pick.pair.call_inst_id
if pick.option_side == "call"
else pick.pair.put_inst_id
)
r = await asyncio.to_thread(
self.matcher.open_group,
group_id=gid,
bias=pick.bias,
option_side=pick.option_side,
perp_side=pick.perp_side,
option_inst_id=option_inst,
entry_index_px=float(entry_idx),
strike=pick.pair.strike,
expiry_ymd=pick.pair.expiry_ymd,
)
if r.ok:
self._set_state(phase="open", last_error=None)
try: