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
+267
View File
@@ -746,6 +746,273 @@ class OkxLiveExecutor(Matcher):
data={"group_id": group_id, "exec_mode": "LIVE"},
)
def open_oo_group(
self,
*,
group_id: str,
call_inst_id: str,
put_inst_id: str,
call_strike: float,
put_strike: float,
entry_index_px: float,
expiry_ymd: str | None = None,
) -> OpenResult:
"""期期 LIVE:先买 Call 再买 Put。"""
err = self._guard_live()
if err:
return OpenResult(ok=False, detail=err)
claimed, claim_msg = claim_open_slot(self.db)
if not claimed:
return OpenResult(ok=False, detail=claim_msg)
safe, safe_msg = assert_safe_to_open_live(self)
if not safe:
release_open_slot_if_opening(self.db)
return OpenResult(ok=False, detail=safe_msg)
s = live_settings()
client = self._client()
opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
call_ct = self._ct_mult(call_inst_id)
put_ct = self._ct_mult(put_inst_id)
call_contracts = contracts_for_eth(opt_qty, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
stamp_opening_intent(
self.db,
group_id=group_id,
option_inst_id=call_inst_id,
option_side="call",
perp_side=f"oo_put:{put_inst_id}",
option_qty_eth=opt_qty,
option_qty_contracts=float(call_contracts),
entry_index_px=entry_index_px,
)
try:
call_fill = client.place_market(
inst_id=call_inst_id,
side="buy",
sz=str(int(round(call_contracts))),
td_mode="cash",
)
except Exception as e:
logger.exception("live oo open call failed")
if "ordId=" not in str(e):
release_open_slot_if_opening(self.db)
return OpenResult(ok=False, detail=f"期期开 Call 失败: {e}")
call_contracts = (
float(call_fill.sz)
if call_fill.sz and call_fill.sz > 0
else float(int(round(call_contracts)))
)
opt_qty = eth_from_contracts(call_contracts, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
try:
put_fill = client.place_market(
inst_id=put_inst_id,
side="buy",
sz=str(int(round(put_contracts))),
td_mode="cash",
)
except Exception as e:
logger.exception("live oo open put failed; rolling back call")
try:
client.place_market(
inst_id=call_inst_id,
side="sell",
sz=str(int(round(call_contracts))),
td_mode="cash",
)
except Exception as e2:
logger.exception("oo call rollback failed: %s", e2)
return OpenResult(
ok=False,
detail=f"期期 Put 失败且 Call 回滚未确认(保留 opening): {e}",
)
release_open_slot_if_opening(self.db)
return OpenResult(ok=False, detail=f"期期开 Put 失败已回滚 Call: {e}")
put_contracts = (
float(put_fill.sz)
if put_fill.sz and put_fill.sz > 0
else float(int(round(put_contracts)))
)
of_px = float(call_fill.avg_px)
pf_px = float(put_fill.avg_px)
call_prem = of_px * opt_qty
put_prem = pf_px * eth_from_contracts(put_contracts, put_ct)
# 等量:以 Call 成交名义为准
qty2 = eth_from_contracts(put_contracts, put_ct)
now = int(time.time() * 1000)
with self.db._lock:
self.db._conn.execute(
"""INSERT INTO groups(
group_id, status, bias, option_side, perp_side, option_inst_id, perp_inst_id,
strike, expiry_ymd, entry_index_px, initial_premium, open_at_ms, fees, slip_cost,
exec_mode, hedge_mode, option2_inst_id, option2_side, strike2, initial_premium2
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"open",
"option_option",
"call",
None,
call_inst_id,
None,
float(call_strike),
expiry_ymd,
entry_index_px,
call_prem,
now,
float(getattr(call_fill, "fee", 0) or 0)
+ float(getattr(put_fill, "fee", 0) or 0),
0.0,
"LIVE",
"option_option",
put_inst_id,
"put",
float(put_strike),
put_prem,
),
)
for leg, inst, contracts, fill_px, fee, ts in (
("option", call_inst_id, call_contracts, of_px, getattr(call_fill, "fee", 0), now),
("option2", put_inst_id, put_contracts, pf_px, getattr(put_fill, "fee", 0), now + 1),
):
q = opt_qty if leg == "option" else qty2
self.db._conn.execute(
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
leg,
"open",
"long",
inst,
q,
contracts,
fill_px,
fill_px,
float(fee or 0),
0.0,
float(fill_px) * float(q),
ts,
"LIVE",
),
)
self.db._conn.execute(
"""UPDATE positions SET
group_id=?, perp_side=NULL, perp_qty_eth=0, perp_entry_px=NULL,
option_inst_id=?, option_side='call', option_qty_eth=?, option_qty_contracts=?,
option_entry_px=?, entry_index_px=?, initial_premium=?, status='open',
hedge_mode='option_option', option2_inst_id=?, option2_side='put',
option2_qty_eth=?, option2_qty_contracts=?, option2_entry_px=?,
strike2=?, initial_premium2=?
WHERE id=1""",
(
group_id,
call_inst_id,
opt_qty,
call_contracts,
of_px,
entry_index_px,
call_prem,
put_inst_id,
qty2,
put_contracts,
pf_px,
float(put_strike),
put_prem,
),
)
self.db._conn.commit()
try:
from ..strategy.exits import lock_trade_exit_target
lock_trade_exit_target(
self.db, group_id=group_id, initial_premium=call_prem + put_prem
)
except Exception:
logger.exception("lock exit oo live failed")
return OpenResult(
ok=True,
group_id=group_id,
detail="opened_oo_live",
data={"hedge_mode": "option_option", "exec_mode": "LIVE"},
)
def live_sell_oo_both(self, *, bypass_liquidity: bool = False) -> None:
"""到期/紧急:交易所市价卖掉 Call+Put。"""
pos = self.current_position()
client = self._client()
for inst, contracts in (
(str(pos.get("option_inst_id") or ""), float(pos.get("option_qty_contracts") or 0)),
(str(pos.get("option2_inst_id") or ""), float(pos.get("option2_qty_contracts") or 0)),
):
if not inst or contracts <= 0:
continue
try:
client.place_market(
inst_id=inst,
side="sell",
sz=str(int(round(contracts))),
td_mode="cash",
)
except Exception:
logger.exception("live_sell_oo_both failed inst=%s", inst)
if not bypass_liquidity:
raise
def close_winning_oo_leave_residual(
self, *, reason: str = "target_oo_win"
) -> CloseResult:
"""期期达标:先标记 closing,再交易所卖掉盈利腿,再落库。"""
err = self._guard_live()
if err:
return CloseResult(ok=False, detail=err)
pos = self.current_position()
if str(pos.get("status") or "") != "open" or not pos.get("option2_inst_id"):
return CloseResult(ok=False, detail="无期期持仓")
# 防重入:已在 closing 则只做账本收尾
if str(pos.get("status") or "") == "closing":
return super().close_winning_oo_leave_residual(
reason=reason, skip_market=True
)
upl = self.unrealized()
call_upl = float(upl.get("option_upl") or 0)
put_upl = float(upl.get("option2_upl") or 0)
if call_upl >= put_upl and call_upl > 0:
win_id = str(pos["option_inst_id"])
win_contracts = float(pos.get("option_qty_contracts") or 0)
elif put_upl > 0:
win_id = str(pos["option2_inst_id"])
win_contracts = float(pos.get("option2_qty_contracts") or 0)
else:
return CloseResult(ok=False, detail="无明确盈利腿")
with self.db._lock:
self.db._conn.execute(
"UPDATE positions SET status='closing' WHERE id=1 AND status='open'"
)
self.db._conn.commit()
client = self._client()
try:
client.place_market(
inst_id=win_id,
side="sell",
sz=str(int(round(win_contracts))),
td_mode="cash",
)
except Exception as e:
with self.db._lock:
self.db._conn.execute(
"UPDATE positions SET status='open' WHERE id=1 AND status='closing'"
)
self.db._conn.commit()
return CloseResult(ok=False, detail=f"期期平盈利腿失败: {e}")
return super().close_winning_oo_leave_residual(
reason=reason, skip_market=True
)
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
err = self._guard_live()
if err: