Fix option-option exit preview to use budget times reward ratio.

Plan no longer shows perp exit_unit*k as the OO target.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 16:41:51 +08:00
parent eaf94448a2
commit 24943adf13
3 changed files with 136 additions and 2 deletions
+109 -1
View File
@@ -714,18 +714,30 @@ def apply_risk_sizing_to_ledger(
return r
def _hedge_mode(ledger: Ledger | None = None) -> str:
led = ledger or Ledger()
s = get_settings()
raw = str(led.get_setting_str("hedge_mode", s.hedge_mode) or s.hedge_mode).strip().lower()
return raw if raw in ("perp_option", "option_option") else "perp_option"
def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]:
"""设置页预览:用当前盘口粗估。"""
"""设置页/计划页预览:用当前盘口粗估。"""
database = db or get_db()
ledger = Ledger(database)
out: dict[str, Any] = {
"sizing_mode": ledger.get_setting_str("sizing_mode", "manual") or "manual",
"risk_based": is_risk_based(ledger),
"hedge_mode": _hedge_mode(ledger),
}
if not is_risk_based(ledger):
out["ok"] = True
out["detail"] = "当前为手动仓位"
return out
if out["hedge_mode"] == "option_option":
return _preview_oo_sizing(database, ledger, out)
try:
from .open_capacity import _index_and_option_ask
@@ -764,3 +776,99 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]:
}
)
return out
def _preview_oo_sizing(
database: Database, ledger: Ledger, out: dict[str, Any]
) -> dict[str, Any]:
"""期期预览:出场目标 = 预算 × 盈亏比;有卖一时再估单腿 qty。"""
s = get_settings()
budget, detail, capital = resolve_budget(database)
mg = resolve_martingale(database, ledger=ledger)
ratio = float(
ledger.get_setting_float("oo_reward_ratio", s.oo_reward_ratio)
or s.oo_reward_ratio
)
cush = float(
ledger.get_setting_float("oo_budget_cushion", s.oo_budget_cushion)
or s.oo_budget_cushion
)
out["martingale"] = mg
out["risk_effective_loss_pct"] = mg.get("effective_pct")
out["reward_ratio"] = ratio
out["cushion"] = cush
if budget is None:
out["ok"] = False
out["detail"] = f"期期预算失败: {detail}"
return out
exit_target = _round2(float(budget) * max(0.5, ratio))
out.update(
{
"budget": _round2(float(budget)),
"capital_base": _round2(float(capital)) if capital is not None else None,
"net_profit_target": exit_target,
"k": None,
"perp_qty_eth": 0.0,
}
)
call_ask = put_ask = idx = None
try:
from .session import get_session
snap = get_session().snapshot()
idx = snap.index_px
if snap.call and snap.call.ask and float(snap.call.ask) > 0:
call_ask = float(snap.call.ask)
if snap.put and snap.put.ask and float(snap.put.ask) > 0:
put_ask = float(snap.put.ask)
if idx is None and snap.perp and snap.perp.mark_px:
idx = float(snap.perp.mark_px)
except Exception:
pass
if call_ask is None or put_ask is None:
try:
from .open_capacity import _index_and_option_ask
i2, a2 = _index_and_option_ask()
if idx is None:
idx = i2
# 回退:单腿 ATM 卖一不够准确,但至少能估数量量级
if call_ask is None and a2 is not None and float(a2) > 0:
call_ask = float(a2)
if put_ask is None and a2 is not None and float(a2) > 0:
put_ask = float(a2)
except Exception:
pass
if call_ask is None or put_ask is None or call_ask <= 0 or put_ask <= 0:
out["ok"] = True
out["detail"] = "已估出场目标;虚值双腿卖一未齐,数量待开仓时再算"
out["option_qty_eth"] = None
out["sizing_ok"] = False
return out
fee_rate = ledger.get_setting_float("fee_rate", s.fee_rate)
r = compute_oo_sizing(
budget=float(budget),
call_ask=float(call_ask),
put_ask=float(put_ask),
fee_rate=fee_rate,
index_px=float(idx or 0),
cushion=cush,
reward_ratio=ratio,
)
# 出场始终按全额预算×盈亏比;数量估失败仍返回 ok 以便 Plan 展示目标
out.update(
{
"ok": True,
"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_ask": r.call_ask,
"put_ask": r.put_ask,
"call_premium": r.call_premium if r.ok else None,
"put_premium": r.put_premium if r.ok else None,
"max_loss": r.max_loss if r.ok else None,
"net_profit_target": exit_target,
"index_px": _round2(float(idx)) if idx is not None else None,
}
)
return out
+23
View File
@@ -66,6 +66,29 @@ def test_amplitude_range_pct() -> None:
assert abs(a.range_pct - 3.0) < 1e-9
def test_preview_oo_exit_budget_times_ratio(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.models.db import Database
from app.sim.ledger import Ledger
from app.strategy.risk_sizing import _preview_oo_sizing
db = Database(tmp_path / "oo_prev.db")
db.set_setting("hedge_mode", "option_option")
db.set_setting("sizing_mode", "risk_based")
db.set_setting("risk_loss_mode", "percent")
db.set_setting("risk_loss_pct", "1")
db.set_setting("risk_capital_source", "manual")
db.set_setting("risk_manual_capital_usdt", "10000")
db.set_setting("oo_reward_ratio", "2")
monkeypatch.setattr("app.strategy.risk_sizing.get_db", lambda: db)
# 无 session 盘口时仍应给出 预算×比
out = _preview_oo_sizing(db, Ledger(db), {"hedge_mode": "option_option"})
assert out.get("ok") is True
assert abs(float(out.get("budget") or 0) - 100.0) < 1e-6
assert abs(float(out.get("net_profit_target") or 0) - 200.0) < 1e-6
db.close()
def test_amplitude_max_gate() -> None:
from app.strategy.oo_selection import build_oo_pick_core
+4 -1
View File
@@ -208,9 +208,12 @@ export default function PlanPage() {
const netPnl = pos?.net_pnl ?? 0;
const movePct = pos?.move_pct ?? 0;
const ooRatio = Number(plan?.oo_reward_ratio ?? 2);
const ooBudget = plan?.risk_sizing_preview?.budget;
const exitRuleLabel = isOo
? exitTarget != null
? `${riskLocked ? "锁定 " : ""}净盈≥${fmt(exitTarget)} U预算×${fmt(ooRatio, 1)}`
? `${riskLocked ? "锁定 " : ""}净盈≥${fmt(exitTarget)} U${
ooBudget != null ? `${fmt(ooBudget)}×` : "预算×"
}${fmt(ooRatio, 1)}`
: `净盈≥预算×${fmt(ooRatio, 1)}(达标只平盈利腿)`
: exitMode === "premium_multiple"
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`