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
+161
View File
@@ -501,6 +501,167 @@ def compute_risk_sizing(
)
@dataclass(frozen=True, slots=True)
class OoSizingResult:
ok: bool
detail: str
budget: float | None = None
spend: float | None = None
qty_eth: float | None = None
call_ask: float | None = None
put_ask: float | None = None
call_premium: float | None = None
put_premium: float | None = None
max_loss: float | None = None
net_profit_target: float | None = None
capital_base: float | None = None
cushion: float | None = None
reward_ratio: float | None = None
def compute_oo_sizing(
*,
budget: float,
call_ask: float,
put_ask: float,
fee_rate: float = 0.0005,
index_px: float = 0.0,
cushion: float = 0.92,
reward_ratio: float = 2.0,
) -> OoSizingResult:
"""
期期 1:1:预算 B 预留后平分两腿权利金;qty 一位小数向下取整;
出场目标 = B × reward_ratio(按全额预算)。
"""
if budget is None or budget <= 0 or not math.isfinite(budget):
return OoSizingResult(ok=False, detail="期期预算无效")
if call_ask <= 0 or put_ask <= 0:
return OoSizingResult(ok=False, detail="期期缺少有效卖一")
cush = min(1.0, max(0.5, float(cushion)))
ratio = max(0.5, float(reward_ratio))
spend = float(budget) * cush
# 粗估两腿开仓费(按指数名义近似)
fee_est = 0.0
if index_px and index_px > 0 and fee_rate > 0:
fee_est = float(index_px) * float(fee_rate) * 2.0
spend_prem = max(0.0, spend - fee_est)
if spend_prem <= 1e-9:
return OoSizingResult(ok=False, detail="期期预留后可用权利金不足")
leg = spend_prem / 2.0
# 等量:受较贵腿限制
q_call = floor_k_1dp(leg / float(call_ask))
q_put = floor_k_1dp(leg / float(put_ask))
qty = min(q_call, q_put)
if qty < 0.1 - 1e-12:
return OoSizingResult(
ok=False,
detail=(
f"期期定仓 qty<{0.1}call可{q_call} put可{q_put}),"
f"预算 {budget:.2f}U 不足"
),
budget=_round2(float(budget)),
)
# 若仍略超 spend_prem,再降一档
while qty >= 0.1 - 1e-12:
cp = float(call_ask) * qty
pp = float(put_ask) * qty
if cp + pp <= spend_prem + 1e-6:
return OoSizingResult(
ok=True,
detail="ok",
budget=_round2(float(budget)),
spend=_round2(spend),
qty_eth=round(qty, 1),
call_ask=_round2(float(call_ask)),
put_ask=_round2(float(put_ask)),
call_premium=_round2(cp),
put_premium=_round2(pp),
max_loss=_round2(cp + pp + fee_est),
net_profit_target=_round2(float(budget) * ratio),
cushion=cush,
reward_ratio=ratio,
)
qty = round(qty - 0.1, 1)
return OoSizingResult(
ok=False,
detail=f"期期无法在预算 {budget:.2f}U 内找到合规 qty",
budget=_round2(float(budget)),
)
def apply_oo_sizing_to_ledger(
*,
call_ask: float,
put_ask: float,
index_px: float,
db: Database | None = None,
) -> OoSizingResult:
database = db or get_db()
ledger = Ledger(database)
s = get_settings()
pos = database.fetchone("SELECT status FROM positions WHERE id=1")
if pos is not None:
st = str(pos["status"] or "flat")
if st in ("open", "half_open", "option_closed_perp_pending", "opening"):
return OoSizingResult(
ok=False,
detail="持仓中已锁定本组成交目标与名义,平仓后再自动计算",
)
budget, detail, capital = resolve_budget(database)
if budget is None:
return OoSizingResult(ok=False, detail=f"期期预算失败: {detail}")
fee_rate = ledger.get_setting_float("fee_rate", s.fee_rate)
cushion = ledger.get_setting_float("oo_budget_cushion", s.oo_budget_cushion)
ratio = ledger.get_setting_float("oo_reward_ratio", s.oo_reward_ratio)
r = compute_oo_sizing(
budget=float(budget),
call_ask=float(call_ask),
put_ask=float(put_ask),
fee_rate=fee_rate,
index_px=float(index_px),
cushion=cushion,
reward_ratio=ratio,
)
if not r.ok:
return r
database.set_setting("exit_mode", "fixed_usdt")
database.set_setting("perp_qty_eth", "0")
database.set_setting("option_qty_eth", str(r.qty_eth))
database.set_setting("net_profit_target", str(r.net_profit_target))
database.set_setting("risk_last_k", str(r.qty_eth))
database.set_setting(
"risk_last_max_loss",
f"{r.max_loss:.2f}" if r.max_loss is not None else "",
)
logger.info(
"oo_sizing applied qty=%.1f call_ask=%.4f put_ask=%.4f exit=%.2f "
"max_loss=%.2f budget=%.2f",
r.qty_eth or 0,
r.call_ask or 0,
r.put_ask or 0,
r.net_profit_target or 0,
r.max_loss or 0,
r.budget or 0,
)
# attach capital for callers
return OoSizingResult(
ok=True,
detail=r.detail,
budget=r.budget,
spend=r.spend,
qty_eth=r.qty_eth,
call_ask=r.call_ask,
put_ask=r.put_ask,
call_premium=r.call_premium,
put_premium=r.put_premium,
max_loss=r.max_loss,
net_profit_target=r.net_profit_target,
capital_base=_round2(capital) if capital is not None else None,
cushion=r.cushion,
reward_ratio=r.reward_ratio,
)
def apply_risk_sizing_to_ledger(
*,
index_px: float,