Add HEDGE_PO_MODE instance modes with shared sim/live path.

Lock one mode per instance/subaccount, cross-asset BTC option/ETH perp sizing, combo-U exit, and ship docs/tests/audit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-14 20:05:25 +08:00
parent ab7a2309ad
commit 5d161b4220
18 changed files with 1215 additions and 115 deletions
+29 -11
View File
@@ -695,6 +695,7 @@ def _tick_po_option_primary(
target_hit,
)
from lib.hedge_plan.hedge_plan_orders_lib import _close_perp
from lib.hedge_plan.hedge_po_mode_lib import combo_net_usdt
perp = next((x for x in legs if x.get("leg_role") == "perp"), None)
opt = next((x for x in legs if x.get("leg_role") == "option_hedge"), None)
@@ -714,7 +715,9 @@ def _tick_po_option_primary(
profit_target = _plan_profit_target(plan)
if strike is None or strike <= 0:
return None
idx = _index_px(cfg, str(plan.get("underlying") or "ETH"))
# 期权指数:计划 underlying(跨币=BTC);目标位同口径
opt_uly = str(plan.get("underlying") or plan.get("option_underlying") or "ETH").upper()
idx = _index_px(cfg, opt_uly)
if idx is None:
return None
@@ -743,6 +746,13 @@ def _tick_po_option_primary(
mark = _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
except Exception:
mark = None
# 跨币:永续标记用 ETH,不可回退到 BTC 指数
if mark is None:
perp_uly = str(plan.get("perp_underlying") or "").upper()
if not perp_uly and symbol:
perp_uly = symbol.split("/")[0].upper()
if perp_uly and perp_uly != opt_uly:
mark = _index_px(cfg, perp_uly)
mark = mark or idx
entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or mark
cs = float(cfg.get("default_contract_size") or 0.01)
@@ -787,10 +797,11 @@ def _tick_po_option_primary(
)
perp_px = _sf(book.get("vwap")) or mark or entry
# 优先期权腿平仓(须买一流动性);盈利金额模式要求扣双边费后净利≥目标
# 优先期权腿平仓(须买一流动性);盈利金额模式要求扣双边费后组合净利(U)≥目标
if hit_opt:
liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=sheets)
net = None
net_usdt = None
if liq_ok:
net = estimate_combo_net_pnl(
view_side=view,
@@ -806,17 +817,19 @@ def _tick_po_option_primary(
contracts=contracts,
contract_size=cs,
)
net_usdt = combo_net_usdt(net, option_index=float(idx))
if exit_mode == "profit":
can_opt_exit = bool(
liq_ok and net is not None and float(net.get("net") or 0) + 1e-9 >= profit_target
liq_ok and net_usdt is not None and float(net_usdt) + 1e-9 >= profit_target
)
else:
can_opt_exit = bool(liq_ok and net is not None and float(net.get("net") or 0) > 0)
# 目标位模式:组合 U 净利须 > 0(与持仓卡同口径)
can_opt_exit = bool(liq_ok and net_usdt is not None and float(net_usdt) > 0)
if can_opt_exit:
reason = "profit_target_usdt" if exit_mode == "profit" else "opt_target_points"
close_r = _sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=sheets)
if close_r.get("already_flat"):
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=net["opt_net"])
opt_pnl_coin = _option_leg_pnl_after_close(cfg, opt, fallback=float(net["opt_net"]))
elif not close_r.get("ok") or not close_r.get("fully_closed", True):
notify_hedge(
cfg,
@@ -829,10 +842,12 @@ def _tick_po_option_primary(
update_plan(conn, int(plan["id"]), close_reason="opt_target_pending")
return {"plan_id": plan["id"], "retry": True, "close": close_r}
else:
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=net["opt_net"])
opt_pnl_coin = _option_leg_pnl_after_close(cfg, opt, fallback=float(net["opt_net"]))
# 币本位期权盈亏折 U(×期权指数),与持仓卡/出场门控同口径
opt_pnl_u = round(float(opt_pnl_coin) * float(idx), 4)
conn.execute(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", reason, _now(), round(opt_pnl, 4), opt["id"]),
("closed", reason, _now(), opt_pnl_u, opt["id"]),
)
perp_close = _close_perp(
cfg, symbol=symbol, direction=perp_dir, contracts=contracts, dry_run=False
@@ -853,14 +868,14 @@ def _tick_po_option_primary(
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
("closed", reason, _now(), round(perp_pnl, 4), perp["id"]),
)
total = float(opt_pnl) + float(perp_pnl)
total = float(net_usdt) if net_usdt is not None else (opt_pnl_u + perp_pnl)
update_plan(
conn,
int(plan["id"]),
status="closed",
close_reason=reason,
realized_pnl_perp=round(perp_pnl, 4),
realized_pnl_options=round(opt_pnl, 4),
realized_pnl_options=opt_pnl_u,
realized_pnl_total=round(total, 4),
stats_bucket="opt_primary",
closed_at=_now(),
@@ -872,6 +887,7 @@ def _tick_po_option_primary(
"close_reason": reason,
"total": total,
"net": net,
"net_usdt": net_usdt,
"sell_coin": sell_r,
}
if exit_mode == "profit":
@@ -881,16 +897,18 @@ def _tick_po_option_primary(
"msg": (
liq_msg
if not liq_ok
else f"净利未达目标 {profit_target:g}(当前 {float((net or {}).get('net') or 0):.2f})"
else f"组合净利(U)未达目标 {profit_target:g}(当前 {float(net_usdt or 0):.2f})"
),
"net": net,
"net_usdt": net_usdt,
}
if not hit_perp:
return {
"plan_id": plan["id"],
"skip": True,
"msg": (liq_msg if not liq_ok else "净利≤0,继续持有"),
"msg": (liq_msg if not liq_ok else "组合净利(U)≤0,继续持有"),
"net": net,
"net_usdt": net_usdt,
}
if not hit_perp: