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