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