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:
+113
-15
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user