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
+78 -18
View File
@@ -777,6 +777,16 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
if q.get("ct_mult") is not None:
body["ct_mult"] = float(q.get("ct_mult") or 0.01)
if is_option_primary(body):
from lib.hedge_plan.hedge_plan_option_primary_lib import contracts_from_option_sheets
from lib.hedge_plan.hedge_po_mode_lib import (
apply_mode_to_start_body,
get_hedge_po_mode,
is_cross_asset,
option_underlying,
perp_underlying,
)
body.update(apply_mode_to_start_body(body))
cs = float(body.get("contract_size") or 0.01)
get_cs = cfg.get("get_contract_size")
sym = str(body.get("exchange_symbol") or "")
@@ -789,20 +799,41 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
ct = float(body.get("ct_mult") or q.get("ct_mult") or 0.01)
ratio = float(body.get("option_perp_ratio") or 2)
body["contract_size"] = cs
opt_idx = None
perp_idx = None
try:
from lib.exchange.okx_public_http_lib import fetch_index_px
mode = str(body.get("po_mode") or get_hedge_po_mode())
opt_idx = float(fetch_index_px(option_underlying(mode)) or 0) or None
if is_cross_asset(mode):
perp_idx = float(fetch_index_px(perp_underlying(mode)) or 0) or None
else:
perp_idx = opt_idx
except Exception:
opt_idx = perp_idx = None
# 页面传入的 entry/index 作回退
if opt_idx is None:
try:
opt_idx = float(body.get("index_px") or body.get("entry") or 0) or None
except (TypeError, ValueError):
opt_idx = None
if perp_idx is None:
perp_idx = opt_idx
if market and float(body.get("sheets") or 0) > 0:
# 手动下单:以张数为准,永续按比例
sheets_i = float(int(float(body["sheets"])))
eth = round(sheets_i * ct, 2)
body["sheets"] = sheets_i
body["eth_qty"] = eth
body["contracts"] = (eth / ratio) / cs if ratio > 0 and cs > 0 else 0
sized = {
"ok": True,
"sheets": sheets_i,
"eth_qty": eth,
"contracts": body["contracts"],
"ratio": ratio,
}
sized = contracts_from_option_sheets(
sheets=float(body["sheets"]),
ct_mult=ct,
ratio=ratio,
contract_size=cs,
option_index=opt_idx,
perp_index=perp_idx,
)
if not sized.get("ok"):
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
body["sheets"] = sized["sheets"]
body["eth_qty"] = sized.get("coin_qty") or sized.get("eth_qty")
body["contracts"] = sized["contracts"]
else:
sized = size_from_premium(
premium_budget=float(body.get("premium_budget") or 0),
@@ -810,21 +841,39 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
ct_mult=ct,
ratio=ratio,
contract_size=cs,
option_index=opt_idx,
perp_index=perp_idx,
)
if not sized.get("ok"):
return {"ok": False, "msg": sized.get("msg") or "定仓失败", "quote": q, "sizing": sized}
body["sheets"] = sized["sheets"]
body["contracts"] = sized["contracts"]
body["eth_qty"] = sized["eth_qty"]
body["eth_qty"] = sized.get("coin_qty") or sized["eth_qty"]
# 深度不足则缩量
ask_sz = float(q.get("ask_sz") or 0)
if ask_sz > 0 and float(body["sheets"]) > ask_sz:
body["sheets"] = float(int(ask_sz))
if body["sheets"] <= 0:
return {"ok": False, "msg": "卖一深度不足 1 张", "quote": q, "sizing": sized}
eth = round(float(body["sheets"]) * ct, 2)
body["eth_qty"] = eth
body["contracts"] = (eth / ratio) / cs if ratio > 0 and cs > 0 else 0
resized = contracts_from_option_sheets(
sheets=float(body["sheets"]),
ct_mult=ct,
ratio=ratio,
contract_size=cs,
option_index=opt_idx,
perp_index=perp_idx,
)
if not resized.get("ok"):
return {"ok": False, "msg": resized.get("msg") or "缩量定仓失败", "quote": q}
body["eth_qty"] = resized.get("coin_qty") or resized.get("eth_qty")
body["contracts"] = resized["contracts"]
sized = resized
cross_note = ""
if sized.get("cross_asset") or sized.get("option_notional_usdt"):
cross_note = (
f" · 名义U 期权{float(sized.get('option_notional_usdt') or 0):.0f}"
f"/永续{float(sized.get('perp_notional_usdt') or 0):.0f}"
)
return {
"ok": True,
"ask": float(q["ask"]),
@@ -837,10 +886,12 @@ def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, An
"msg": (
f"手动定仓: {body.get('eth_qty')}币 / "
f"{body.get('sheets')}张期权 / {float(body.get('contracts') or 0):.4f}张永续 @{q['ask']}"
f"{cross_note}"
if market
else (
f"期权为主定仓: 权利金×0.95→{body.get('eth_qty')}ETH / "
f"期权为主定仓: 权利金×0.95→{body.get('eth_qty')}币 / "
f"{body.get('sheets')}张期权 / {float(body.get('contracts') or 0):.4f}张永续 @{q['ask']}"
f"{cross_note}"
)
),
}
@@ -1184,7 +1235,16 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
is_option_primary,
validate_option_primary_start,
)
from lib.hedge_plan.hedge_po_mode_lib import (
apply_mode_to_start_body,
validate_body_matches_mode,
)
# 模拟/实盘同路径:启动前按实例模式钳制
body.update(apply_mode_to_start_body(body))
mode_err = validate_body_matches_mode(body)
if mode_err:
return mode_err
if is_option_primary(body):
from lib.hedge_plan.hedge_plan_option_primary_lib import validate_option_primary_watch