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