Split OO budget per leg and size Call/Put independently.

Each half of the risk budget buys its own qty from ask; equal qty no longer forced.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 17:08:05 +08:00
parent 24943adf13
commit 24a860ad3f
11 changed files with 210 additions and 110 deletions
+7 -1
View File
@@ -116,6 +116,7 @@ class StrategyEngine:
fixed_perp = "long"
perp_qty = self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth)
opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
oo_put_qty = self.ledger.get_setting_float("oo_put_qty_eth", opt_qty)
sizing_mode = (
sm
if (
@@ -161,8 +162,12 @@ class StrategyEngine:
if risk_preview.get("ok"):
if risk_preview.get("perp_qty_eth") is not None:
perp_qty = float(risk_preview["perp_qty_eth"])
if risk_preview.get("option_qty_eth") is not None:
if risk_preview.get("call_qty_eth") is not None:
opt_qty = float(risk_preview["call_qty_eth"])
elif risk_preview.get("option_qty_eth") is not None:
opt_qty = float(risk_preview["option_qty_eth"])
if risk_preview.get("put_qty_eth") is not None:
oo_put_qty = float(risk_preview["put_qty_eth"])
if risk_preview.get("net_profit_target") is not None:
net_target = float(risk_preview["net_profit_target"])
exit_amt = net_target
@@ -243,6 +248,7 @@ class StrategyEngine:
"perp_margin_mode": perp_mm,
"perp_qty_eth": perp_qty,
"option_qty_eth": opt_qty,
"oo_put_qty_eth": oo_put_qty,
"sizing_mode": sizing_mode,
"risk_based": sizing_mode == "risk_based",
"hedge_mode": (
+10 -9
View File
@@ -152,16 +152,17 @@ def assess_open_capacity(
ca = float(snap.call.ask)
if pa is None and snap.put and snap.put.ask:
pa = float(snap.put.ask)
except Exception:
pass
cush = float(
ledger.get_setting_float("oo_budget_cushion", s.oo_budget_cushion)
or s.oo_budget_cushion
)
cush = min(1.0, max(0.5, cush))
except Exception:
pass
if ca is not None and pa is not None and ca > 0 and pa > 0:
# 与定仓一致:按预留后的权利金需求估资金门
premium_need = (ca + pa) * opt_qty * (1.0 + fee_rate) * cush
call_q = float(opt_qty)
put_q = float(
ledger.get_setting_float("oo_put_qty_eth", call_q) or call_q
)
# 与定仓一致:两腿各自权利金
premium_need = (
(ca * call_q + pa * put_q) * (1.0 + fee_rate)
)
else:
premium_need = None
margin_need = 0.0
+64 -46
View File
@@ -507,7 +507,9 @@ class OoSizingResult:
detail: str
budget: float | None = None
spend: float | None = None
qty_eth: float | None = None
qty_eth: float | None = None # 兼容:Call 数量
call_qty_eth: float | None = None
put_qty_eth: float | None = None
call_ask: float | None = None
put_ask: float | None = None
call_premium: float | None = None
@@ -517,6 +519,7 @@ class OoSizingResult:
capital_base: float | None = None
cushion: float | None = None
reward_ratio: float | None = None
leg_budget: float | None = None # 单腿权利金预算(B/2×cushion)
def compute_oo_sizing(
@@ -530,7 +533,8 @@ def compute_oo_sizing(
reward_ratio: float = 2.0,
) -> OoSizingResult:
"""
期期 1:1:预算 B 预留后平分两腿权利金;qty 一位小数向下取整
期期:总预算 B 平分给 Call/Put(各约 B/2,再乘 cushion 预留)
两腿按各自卖一独立定仓 qty=floor_1dp(腿预算/ask),数量可以不同;
出场目标 = B × reward_ratio(按全额预算)。
"""
if budget is None or budget <= 0 or not math.isfinite(budget):
@@ -539,53 +543,56 @@ def compute_oo_sizing(
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
# 各腿:总预算一半 × 预留
leg_raw = float(budget) / 2.0
leg_budget = leg_raw * cush
# 单腿开仓费粗估(从该腿预算里扣)
fee_one = 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:
fee_one = float(index_px) * float(fee_rate)
leg_spend = max(0.0, leg_budget - fee_one)
if leg_spend <= 1e-9:
return OoSizingResult(ok=False, detail="期期单腿预留后可用权利金不足")
def _leg_qty(ask: float) -> tuple[float, float]:
q = floor_k_1dp(leg_spend / float(ask))
while q >= 0.1 - 1e-12:
prem = float(ask) * q
if prem <= leg_spend + 1e-6:
return round(q, 1), prem
q = round(q - 0.1, 1)
return 0.0, 0.0
q_call, cp = _leg_qty(float(call_ask))
q_put, pp = _leg_qty(float(put_ask))
if q_call < 0.1 - 1e-12 or q_put < 0.1 - 1e-12:
return OoSizingResult(
ok=False,
detail=(
f"期期定仓 qty<{0.1}call可{q_call} put可{q_put}),"
f"预算 {budget:.2f}U 不足"
f"期期定仓失败:Call可{q_call} Put可{q_put}(各腿预算约"
f"{leg_budget:.2f}U),总预算 {budget:.2f}U 不足"
),
budget=_round2(float(budget)),
leg_budget=_round2(leg_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)
spend = leg_budget * 2.0
return OoSizingResult(
ok=False,
detail=f"期期无法在预算 {budget:.2f}U 内找到合规 qty",
ok=True,
detail="ok",
budget=_round2(float(budget)),
spend=_round2(spend),
qty_eth=round(q_call, 1),
call_qty_eth=round(q_call, 1),
put_qty_eth=round(q_put, 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_one * 2.0),
net_profit_target=_round2(float(budget) * ratio),
cushion=cush,
reward_ratio=ratio,
leg_budget=_round2(leg_budget),
)
@@ -624,24 +631,29 @@ def apply_oo_sizing_to_ledger(
)
if not r.ok:
return r
call_q = float(r.call_qty_eth or r.qty_eth or 0)
put_q = float(r.put_qty_eth or r.qty_eth or 0)
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("option_qty_eth", str(call_q))
database.set_setting("oo_put_qty_eth", str(put_q))
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_k", str(call_q))
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,
"oo_sizing applied call_qty=%.1f put_qty=%.1f call_ask=%.4f put_ask=%.4f "
"exit=%.2f max_loss=%.2f budget=%.2f leg=%.2f",
call_q,
put_q,
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,
r.leg_budget or 0,
)
# attach capital for callers
return OoSizingResult(
@@ -649,7 +661,9 @@ def apply_oo_sizing_to_ledger(
detail=r.detail,
budget=r.budget,
spend=r.spend,
qty_eth=r.qty_eth,
qty_eth=call_q,
call_qty_eth=call_q,
put_qty_eth=put_q,
call_ask=r.call_ask,
put_ask=r.put_ask,
call_premium=r.call_premium,
@@ -659,6 +673,7 @@ def apply_oo_sizing_to_ledger(
capital_base=_round2(capital) if capital is not None else None,
cushion=r.cushion,
reward_ratio=r.reward_ratio,
leg_budget=r.leg_budget,
)
@@ -862,6 +877,9 @@ def _preview_oo_sizing(
"sizing_ok": bool(r.ok),
"detail": "ok" if r.ok else str(r.detail or "期期数量未估出"),
"option_qty_eth": r.qty_eth if r.ok else None,
"call_qty_eth": r.call_qty_eth if r.ok else None,
"put_qty_eth": r.put_qty_eth if r.ok else None,
"leg_budget": r.leg_budget if r.ok else None,
"call_ask": r.call_ask,
"put_ask": r.put_ask,
"call_premium": r.call_premium if r.ok else None,