Show semi risk-based open size from ask and unit ratio.

Semi sizing uses market ask with semi units; Plan panel previews option/perp qty under the form.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 14:27:19 +08:00
parent 7db8b724ac
commit b09d1b0886
9 changed files with 343 additions and 44 deletions
+3
View File
@@ -254,6 +254,9 @@ class StrategyEngine:
"oo_put_qty_eth": oo_put_qty,
"sizing_mode": sizing_mode,
"risk_based": sizing_mode == "risk_based",
"fee_rate": float(
self.ledger.get_setting_float("fee_rate", s.fee_rate) or s.fee_rate
),
"hedge_mode": (
hm
if (
+7
View File
@@ -83,10 +83,15 @@ def size_and_gate(
led = Ledger(database)
semi_perp = None
semi_opt = None
semi_exit = None
semi_basis = None
if is_semi_auto(led):
sp = read_semi_params(led)
semi_perp = float(sp["perp_unit"])
semi_opt = float(sp["option_unit"])
semi_exit = float(sp["perp_exit_unit"])
# 半自动以损定仓:按盘口卖一推期权量,再按配比乘 k 得永续量
semi_basis = "actual"
if not is_risk_based(led):
# 手动仓:半自动直接写入 永续:期权 配比名义
database.set_setting("perp_qty_eth", f"{semi_perp:.4f}")
@@ -97,6 +102,8 @@ def size_and_gate(
db=database,
perp_unit=semi_perp,
option_unit=semi_opt,
exit_unit=semi_exit,
leverage_basis=semi_basis,
)
if not rs.ok:
return OpenPrepResult(ok=False, detail=rs.detail, sizing_detail=rs.detail)
+52 -5
View File
@@ -430,6 +430,8 @@ def compute_risk_sizing(
db: Database | None = None,
perp_unit: float | None = None,
option_unit: float | None = None,
exit_unit: float | None = None,
leverage_basis: str | None = None,
) -> RiskSizingResult:
database = db or get_db()
ledger = Ledger(database)
@@ -440,9 +442,16 @@ def compute_risk_sizing(
perp_u = float(perp_unit)
if option_unit is not None and float(option_unit) > 0:
opt_u = float(option_unit)
basis_raw = ledger.get_setting_str(
"risk_leverage_basis", s.risk_leverage_basis
) or s.risk_leverage_basis
if exit_unit is not None and float(exit_unit) > 0:
exit_u = float(exit_unit)
basis_raw = (
leverage_basis
if leverage_basis is not None
else (
ledger.get_setting_str("risk_leverage_basis", s.risk_leverage_basis)
or s.risk_leverage_basis
)
)
min_opt_lev = ledger.get_setting_float(
"min_option_leverage", s.min_option_leverage
)
@@ -690,6 +699,8 @@ def apply_risk_sizing_to_ledger(
db: Database | None = None,
perp_unit: float | None = None,
option_unit: float | None = None,
exit_unit: float | None = None,
leverage_basis: str | None = None,
) -> RiskSizingResult:
"""计算并写入 perp/option/exit;非以损定仓模式直接 ok 跳过。持仓中拒绝改写。"""
database = db or get_db()
@@ -713,6 +724,8 @@ def apply_risk_sizing_to_ledger(
db=database,
perp_unit=perp_unit,
option_unit=option_unit,
exit_unit=exit_unit,
leverage_basis=leverage_basis,
)
if not r.ok:
return r
@@ -793,8 +806,38 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]:
out["ok"] = False
out["detail"] = "暂无指数或期权卖一,无法预览"
return out
r = compute_risk_sizing(index_px=float(idx), option_ask=float(ask), db=database)
# 半自动:用本单配比/净利基数,并以盘口卖一定仓(与开仓一致)
semi_perp = semi_opt = semi_exit = None
semi_on = False
try:
from .semi_auto import is_semi_auto, read_semi_params
if is_semi_auto(ledger):
sp = read_semi_params(ledger)
semi_on = True
semi_perp = float(sp["perp_unit"])
semi_opt = float(sp["option_unit"])
semi_exit = float(sp["perp_exit_unit"])
except Exception:
logger.debug("preview semi units skipped", exc_info=True)
r = compute_risk_sizing(
index_px=float(idx),
option_ask=float(ask),
db=database,
perp_unit=semi_perp,
option_unit=semi_opt,
exit_unit=semi_exit,
leverage_basis="actual" if semi_on else None,
)
perp_u, opt_u, exit_u = read_risk_units(ledger)
if semi_perp is not None:
perp_u = float(semi_perp)
if semi_opt is not None:
opt_u = float(semi_opt)
if semi_exit is not None:
exit_u = float(semi_exit)
mg = resolve_martingale(database, ledger=ledger)
s = get_settings()
exit_mode = str(
@@ -806,7 +849,10 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]:
)
exit_target = r.net_profit_target
exit_label = "基数×k"
if exit_mode == "premium_multiple":
if semi_on:
exit_label = "半自动净利基数×k"
exit_mode = "fixed_usdt"
elif exit_mode == "premium_multiple":
exit_label = f"权利金×{prem_mult:g}"
if r.ok and r.premium_est is not None:
exit_target = round(float(r.premium_est) * max(0.0, prem_mult), 2)
@@ -833,6 +879,7 @@ def preview_risk_sizing(db: Database | None = None) -> dict[str, Any]:
"perp_unit": perp_u,
"option_unit": opt_u,
"exit_unit": exit_u,
"semi_units": semi_on,
"martingale": mg,
"risk_effective_loss_pct": mg.get("effective_pct"),
}
+35
View File
@@ -119,6 +119,41 @@ def test_selection_basis_yields_smaller_k_when_ask_cheap() -> None:
assert r_act.k is not None and r_act.k > r_sel.k
def test_compute_risk_sizing_unit_overrides(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.models.db import Database
from app.strategy.risk_sizing import compute_risk_sizing
db = Database(tmp_path / "risk_units.db")
db.set_setting("sizing_mode", "risk_based")
db.set_setting("risk_loss_mode", "absolute")
db.set_setting("risk_loss_usdt", "83")
db.set_setting("fee_rate", "0.0005")
db.set_setting("risk_leverage_basis", "selection")
db.set_setting("min_option_leverage", "100")
db.set_setting("risk_perp_unit", "1")
db.set_setting("risk_option_unit", "2")
db.set_setting("risk_exit_unit", "15")
# 覆盖为单位 0.5:4、出场 5,并强制实际卖一(ask=20 → cost=80+3=83 → k=1
r = compute_risk_sizing(
index_px=2000.0,
option_ask=20.0,
db=db,
perp_unit=0.5,
option_unit=4.0,
exit_unit=5.0,
leverage_basis="actual",
)
assert r.ok
assert r.leverage_basis == "actual"
assert r.k == 1.0
assert r.perp_qty_eth == 0.5
assert r.option_qty_eth == 4.0
assert r.net_profit_target == 5.0
db.close()
def test_compute_risk_sizing_respects_basis(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.models.db import Database