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
+30 -13
View File
@@ -743,18 +743,19 @@ class BinanceLiveExecutor(Matcher):
s = live_settings()
client = self._client()
opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
call_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
put_qty = self.ledger.get_setting_float("oo_put_qty_eth", call_qty)
call_ct = self._ct_mult(call_inst_id)
put_ct = self._ct_mult(put_inst_id)
call_contracts = contracts_for_eth(opt_qty, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
call_contracts = contracts_for_eth(call_qty, call_ct)
put_contracts = contracts_for_eth(put_qty, put_ct)
stamp_opening_intent(
self.db,
group_id=group_id,
option_inst_id=call_inst_id,
option_side="call",
perp_side=f"oo_put:{put_inst_id}",
option_qty_eth=opt_qty,
option_qty_eth=call_qty,
option_qty_contracts=float(call_contracts),
entry_index_px=entry_index_px,
)
@@ -771,8 +772,8 @@ class BinanceLiveExecutor(Matcher):
if call_fill.sz and call_fill.sz > 0
else float(call_contracts)
)
opt_qty = eth_from_contracts(call_contracts, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
call_qty = eth_from_contracts(call_contracts, call_ct)
put_contracts = contracts_for_eth(put_qty, put_ct)
try:
put_fill = client.place_option_market(
symbol=put_inst_id, side="BUY", quantity=put_contracts
@@ -798,9 +799,9 @@ class BinanceLiveExecutor(Matcher):
)
of_px = float(call_fill.avg_px)
pf_px = float(put_fill.avg_px)
qty2 = eth_from_contracts(put_contracts, put_ct)
call_prem = of_px * opt_qty
put_prem = pf_px * qty2
put_qty = eth_from_contracts(put_contracts, put_ct)
call_prem = of_px * call_qty
put_prem = pf_px * put_qty
now = int(time.time() * 1000)
with self.db._lock:
self.db._conn.execute(
@@ -834,8 +835,24 @@ class BinanceLiveExecutor(Matcher):
),
)
for leg, inst, contracts, fill_px, fee, ts, q in (
("option", call_inst_id, call_contracts, of_px, getattr(call_fill, "fee", 0), now, opt_qty),
("option2", put_inst_id, put_contracts, pf_px, getattr(put_fill, "fee", 0), now + 1, qty2),
(
"option",
call_inst_id,
call_contracts,
of_px,
getattr(call_fill, "fee", 0),
now,
call_qty,
),
(
"option2",
put_inst_id,
put_contracts,
pf_px,
getattr(put_fill, "fee", 0),
now + 1,
put_qty,
),
):
self.db._conn.execute(
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
@@ -870,13 +887,13 @@ class BinanceLiveExecutor(Matcher):
(
group_id,
call_inst_id,
opt_qty,
call_qty,
call_contracts,
of_px,
entry_index_px,
call_prem,
put_inst_id,
qty2,
put_qty,
put_contracts,
pf_px,
float(put_strike),
+33 -16
View File
@@ -771,18 +771,19 @@ class OkxLiveExecutor(Matcher):
s = live_settings()
client = self._client()
opt_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
call_qty = self.ledger.get_setting_float("option_qty_eth", s.option_qty_eth)
put_qty = self.ledger.get_setting_float("oo_put_qty_eth", call_qty)
call_ct = self._ct_mult(call_inst_id)
put_ct = self._ct_mult(put_inst_id)
call_contracts = contracts_for_eth(opt_qty, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
call_contracts = contracts_for_eth(call_qty, call_ct)
put_contracts = contracts_for_eth(put_qty, put_ct)
stamp_opening_intent(
self.db,
group_id=group_id,
option_inst_id=call_inst_id,
option_side="call",
perp_side=f"oo_put:{put_inst_id}",
option_qty_eth=opt_qty,
option_qty_eth=call_qty,
option_qty_contracts=float(call_contracts),
entry_index_px=entry_index_px,
)
@@ -804,8 +805,9 @@ class OkxLiveExecutor(Matcher):
if call_fill.sz and call_fill.sz > 0
else float(int(round(call_contracts)))
)
opt_qty = eth_from_contracts(call_contracts, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
call_qty = eth_from_contracts(call_contracts, call_ct)
# Put 用独立定仓数量,不跟 Call 成交量对齐
put_contracts = contracts_for_eth(put_qty, put_ct)
try:
put_fill = client.place_market(
inst_id=put_inst_id,
@@ -838,10 +840,10 @@ class OkxLiveExecutor(Matcher):
)
of_px = float(call_fill.avg_px)
pf_px = float(put_fill.avg_px)
call_prem = of_px * opt_qty
put_prem = pf_px * eth_from_contracts(put_contracts, put_ct)
# 等量:以 Call 成交名义为准
qty2 = eth_from_contracts(put_contracts, put_ct)
call_qty = eth_from_contracts(call_contracts, call_ct)
put_qty = eth_from_contracts(put_contracts, put_ct)
call_prem = of_px * call_qty
put_prem = pf_px * put_qty
now = int(time.time() * 1000)
with self.db._lock:
self.db._conn.execute(
@@ -874,11 +876,26 @@ class OkxLiveExecutor(Matcher):
put_prem,
),
)
for leg, inst, contracts, fill_px, fee, ts in (
("option", call_inst_id, call_contracts, of_px, getattr(call_fill, "fee", 0), now),
("option2", put_inst_id, put_contracts, pf_px, getattr(put_fill, "fee", 0), now + 1),
for leg, inst, contracts, fill_px, fee, ts, q in (
(
"option",
call_inst_id,
call_contracts,
of_px,
getattr(call_fill, "fee", 0),
now,
call_qty,
),
(
"option2",
put_inst_id,
put_contracts,
pf_px,
getattr(put_fill, "fee", 0),
now + 1,
put_qty,
),
):
q = opt_qty if leg == "option" else qty2
self.db._conn.execute(
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
@@ -912,13 +929,13 @@ class OkxLiveExecutor(Matcher):
(
group_id,
call_inst_id,
opt_qty,
call_qty,
call_contracts,
of_px,
entry_index_px,
call_prem,
put_inst_id,
qty2,
put_qty,
put_contracts,
pf_px,
float(put_strike),
+16 -14
View File
@@ -426,22 +426,23 @@ class Matcher:
return OpenResult(ok=False, detail="期期 Call/Put 卖一不可用")
fee_rate = self._fee_rate()
opt_qty = self.ledger.get_setting_float("option_qty_eth", 0.1)
if opt_qty < 0.1 - 1e-12:
call_qty = self.ledger.get_setting_float("option_qty_eth", 0.1)
put_qty = self.ledger.get_setting_float("oo_put_qty_eth", call_qty)
if call_qty < 0.1 - 1e-12 or put_qty < 0.1 - 1e-12:
return OpenResult(ok=False, detail="期期名义 qty 无效")
call_ct = self._ct_mult(call_inst_id)
put_ct = self._ct_mult(put_inst_id)
call_contracts = contracts_for_eth(opt_qty, call_ct)
put_contracts = contracts_for_eth(opt_qty, put_ct)
call_contracts = contracts_for_eth(call_qty, call_ct)
put_contracts = contracts_for_eth(put_qty, put_ct)
cf = option_fill(
action="open",
bid=float(getattr(cq, "bid", None) or 0),
ask=float(cq.ask),
qty_eth=opt_qty,
qty_eth=call_qty,
fee_rate=fee_rate,
)
call_prem = cf.fill_px * opt_qty
call_prem = cf.fill_px * call_qty
call_cost = cf.notional + cf.fee
try:
self.ledger.apply_cash(
@@ -460,10 +461,10 @@ class Matcher:
action="open",
bid=float(getattr(pq2, "bid", None) or 0),
ask=ask2,
qty_eth=opt_qty,
qty_eth=put_qty,
fee_rate=fee_rate,
)
put_prem = pf.fill_px * opt_qty
put_prem = pf.fill_px * put_qty
put_cost = pf.notional + pf.fee
try:
self.ledger.apply_cash(
@@ -479,7 +480,7 @@ class Matcher:
action="close",
bid=bid,
ask=float(cq.ask),
qty_eth=opt_qty,
qty_eth=call_qty,
fee_rate=fee_rate,
)
self.ledger.apply_cash(
@@ -532,7 +533,7 @@ class Matcher:
"open",
"long",
call_inst_id,
opt_qty,
call_qty,
call_contracts,
cf.base_px,
cf.fill_px,
@@ -553,7 +554,7 @@ class Matcher:
"open",
"long",
put_inst_id,
opt_qty,
put_qty,
put_contracts,
pf.base_px,
pf.fill_px,
@@ -576,7 +577,7 @@ class Matcher:
group_id,
call_inst_id,
"call",
opt_qty,
call_qty,
call_contracts,
cf.fill_px,
entry_index_px,
@@ -585,7 +586,7 @@ class Matcher:
"option_option",
put_inst_id,
"put",
opt_qty,
put_qty,
put_contracts,
pf.fill_px,
float(put_strike),
@@ -614,7 +615,8 @@ class Matcher:
"put_inst_id": put_inst_id,
"call_strike": float(call_strike),
"put_strike": float(put_strike),
"option_qty_eth": float(opt_qty),
"option_qty_eth": float(call_qty),
"option2_qty_eth": float(put_qty),
"initial_premium": total_prem,
"fees": cf.fee + pf.fee,
"open_sequence": ["call", "put"],
+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,