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:
@@ -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),
|
||||
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
@@ -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": (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,6 +46,25 @@ def test_select_oo_pair_same_expiry(tmp_path=None) -> None:
|
||||
|
||||
|
||||
def test_compute_oo_sizing_1_1_and_reward() -> None:
|
||||
r = compute_oo_sizing(
|
||||
budget=100.0,
|
||||
call_ask=5.0,
|
||||
put_ask=10.0, # Put 更贵 → 数量更少
|
||||
fee_rate=0.0,
|
||||
index_px=2000.0,
|
||||
cushion=1.0, # 测纯平分,不预留
|
||||
reward_ratio=2.0,
|
||||
)
|
||||
assert r.ok
|
||||
# 各 50U:Call 50/5=10,Put 50/10=5
|
||||
assert r.call_qty_eth == 10.0
|
||||
assert r.put_qty_eth == 5.0
|
||||
assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9
|
||||
assert abs(float(r.call_premium or 0) - 50.0) < 1e-6
|
||||
assert abs(float(r.put_premium or 0) - 50.0) < 1e-6
|
||||
|
||||
|
||||
def test_compute_oo_sizing_equal_asks() -> None:
|
||||
r = compute_oo_sizing(
|
||||
budget=100.0,
|
||||
call_ask=5.0,
|
||||
@@ -56,7 +75,9 @@ def test_compute_oo_sizing_1_1_and_reward() -> None:
|
||||
reward_ratio=2.0,
|
||||
)
|
||||
assert r.ok
|
||||
assert r.qty_eth == 9.2
|
||||
# 各腿 46U / 5 = 9.2
|
||||
assert r.call_qty_eth == 9.2
|
||||
assert r.put_qty_eth == 9.2
|
||||
assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9
|
||||
assert float(r.call_premium or 0) + float(r.put_premium or 0) <= 92.0 + 1e-6
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
## 定仓
|
||||
|
||||
- 仅支持以损定仓 + 亏损幅度 %(含倍投)。
|
||||
- 预算 B 预留余地后两腿 **1:1** 平分权利金;数量 **一位小数向下取整**。
|
||||
- 预算 B 平分给 Call/Put(各约 B/2,再乘 cushion);两腿按各自卖一 **独立** 定仓(数量可不同)。
|
||||
- 出场目标按全额 B × 盈亏比(例 B=100、比=2 → 目标 200U)。
|
||||
|
||||
## 平仓
|
||||
|
||||
@@ -367,6 +367,7 @@ export type PlanState = {
|
||||
oo_min_option_hours?: number;
|
||||
oo_min_leverage?: number;
|
||||
oo_reward_ratio?: number;
|
||||
oo_put_qty_eth?: number;
|
||||
sizing_mode?: "manual" | "risk_based";
|
||||
risk_based?: boolean;
|
||||
ledger: { equity: number; available: number; reserved: number };
|
||||
|
||||
@@ -223,7 +223,7 @@ export default function PlanPage() {
|
||||
: `待估算(基数${fmt(plan?.risk_exit_unit ?? 15)})`
|
||||
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
|
||||
const riskRatioLabel = isOo
|
||||
? "Call:Put 1:1"
|
||||
? "预算平分 Call/Put"
|
||||
: `比例${Number(plan?.risk_perp_unit ?? 1)}:${Number(plan?.risk_option_unit ?? 2)}`;
|
||||
const sizingModeLabel = riskBased
|
||||
? [
|
||||
@@ -268,7 +268,16 @@ export default function PlanPage() {
|
||||
"虚值Call@高·Put@低",
|
||||
`剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`,
|
||||
`杠杆≥${fmt(plan?.oo_min_leverage ?? 200, 0)}x`,
|
||||
displayOptQty != null ? `单腿${fmt(displayOptQty, 1)}ETH` : null,
|
||||
displayOptQty != null
|
||||
? `Call${fmt(displayOptQty, 1)}/Put${fmt(
|
||||
Number(
|
||||
plan?.risk_sizing_preview?.put_qty_eth ??
|
||||
plan?.oo_put_qty_eth ??
|
||||
displayOptQty,
|
||||
),
|
||||
1,
|
||||
)}ETH`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
@@ -756,7 +756,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
|
||||
目标盈利 = 以损预算 × 盈亏比(例预算 100U、比 2 → 目标
|
||||
200U)。两腿 1:1 平分预算。振幅/选约参数见「选约」页。
|
||||
200U)。预算平分给 Call/Put,各按卖一独立定仓。
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -785,7 +785,7 @@ export default function SettingsPage() {
|
||||
<div className="field">
|
||||
<label>定仓模式</label>
|
||||
<div className="mono" style={{ fontSize: 13 }}>
|
||||
以损定仓(期期固定 · Call/Put 1:1)
|
||||
以损定仓(期期固定 · 预算平分,按各自卖一定仓)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1048,11 +1048,19 @@ export default function SettingsPage() {
|
||||
? ` · 倍投待命(连亏${Number(mg.loss_days) || 0}天)`
|
||||
: "";
|
||||
return isOo
|
||||
? `预算=${budS}U · 估亏=${mxS}U · 出场=预算×${ooRewardRatio}${
|
||||
riskPreview.option_qty_eth != null
|
||||
? ` · 单腿≈${riskPreview.option_qty_eth}ETH`
|
||||
: ""
|
||||
}${mgS}`
|
||||
? `预算=${budS}U · 各腿≈${
|
||||
riskPreview.leg_budget != null
|
||||
? Number(riskPreview.leg_budget).toFixed(2)
|
||||
: budS !== "—"
|
||||
? (Number(budS) / 2).toFixed(2)
|
||||
: "—"
|
||||
}U · Call≈${
|
||||
riskPreview.call_qty_eth ??
|
||||
riskPreview.option_qty_eth ??
|
||||
"—"
|
||||
} · Put≈${
|
||||
riskPreview.put_qty_eth ?? "—"
|
||||
} · 出场=预算×${ooRewardRatio}${mgS}`
|
||||
: `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exit ?? "—"}${basisS}${mgS}`;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user