feat(gui): Strategia pannello P/L con slider sizing + fix max_loss

Pannello "P/L atteso — Conservativa vs Aggressiva":
* Sostituiti slider Capitale/Spot con slider parametrici Cap/trade
  (EUR) + posizioni concorrenti. Il capitale richiesto viene calcolato
  in automatico via Kelly-binding aggregato:
  capital = cap_pertrade_usd × concorrenza / max(kelly, 1e-3).
* Profili Conservativa/Aggressiva ora ereditano dai yaml SOLO le leve
  qualitative (width_pct, credit_ratio, kelly_fraction, feature
  attive); le leve di sizing (cap, concorrenza) sono comandate dagli
  slider per confronti omogenei.
* Tre metriche header: capitale richiesto, cap aggregato notional,
  cap per trade USD.

Fix in `_compute_pl`:
* Max loss per contratto era `width` (errato per credit spread).
  Corretto a `width − credit` allineato a core/sizing_engine.py.
  Effetto: n_kelly aumenta proporzionalmente al credit incassato →
  P/L stimato più realistico per spread con credit_to_width_ratio
  alto (es. 0.30+ in profilo Aggressiva).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-05-10 09:00:42 +00:00
parent efa829f7aa
commit e978a44bff
+60 -13
View File
@@ -476,7 +476,11 @@ def _compute_pl(
cap_pertrade_usd = caps["cap_pertrade_eur"] * eur_to_usd
risk_target = min(caps["kelly"] * capital, cap_pertrade_usd)
n_kelly = int(risk_target // width) if width > 0 else 0
# Max loss per contratto = width credit (NON width). Su un put
# spread incassi `credit` upfront, quindi la perdita massima è la
# larghezza meno il credito (vedi core/sizing_engine.py).
max_loss_per_contract = max(width - credit, 1e-6)
n_kelly = int(risk_target // max_loss_per_contract)
n_per_trade = max(0, min(n_kelly, int(caps["max_n"])))
prob_time_stop = 0.07
@@ -670,27 +674,33 @@ def _render_pl_panel(
"""Pannello P/L: confronto Conservativa vs Aggressiva sugli stessi slider."""
st.subheader("💰 P/L atteso — Conservativa vs Aggressiva")
st.caption(
"Stessi slider, due profili di sizing. **Conservativa** = la "
"golden config attuale (`strategy.yaml`). **Aggressiva** = "
"`strategy.aggressiva.yaml` con cap_per_trade 4×, max contratti "
"4×, 2 posizioni concorrenti. Le regole §2-§9 sono identiche; "
"cambiano SOLO le leve di sizing — quello che il P/L "
"conservativo lascia sul tavolo."
"Slider parametrici: scegli **cap per trade** e **posizioni "
"concorrenti**, il capitale richiesto viene calcolato in "
"automatico (Kelly-binding × concurrency / kelly_fraction). "
"Conservativa e Aggressiva ereditano dai rispettivi yaml SOLO "
"le leve qualitative (width_pct, credit_ratio, kelly_fraction, "
"feature attive); le leve di sizing (cap, concorrenza) le "
"controlli qui sotto."
)
col_a, col_b, col_c, col_d = st.columns(4)
capital = col_a.slider(
"Capitale (USD)", 720, 50_000, value=10_000, step=100
col_a, col_b, col_c, col_d, col_e = st.columns(5)
cap_per_trade_eur = col_a.slider(
"Cap/trade (EUR)", 50, 2000, value=200, step=10,
help="Massima perdita per singolo trade. Bound al rischio.",
)
spot = col_b.slider("Spot ETH (USD)", 1500, 6000, value=3000, step=100)
win_rate = col_c.slider(
concurrency_override = col_b.slider(
"Pos. concorrenti", 1, 10, value=3, step=1,
help="Quanti trade simultanei. Cap aggregato = cap/trade × N.",
)
spot = col_c.slider("Spot ETH (USD)", 1500, 6000, value=3000, step=100)
win_rate = col_d.slider(
"Win rate atteso", 0.50, 0.90, value=0.75, step=0.01,
help=(
"Senza filtri quant ≈ 0.650.70. CON filtri (dealer gamma>0, "
"no macro, IVRV>0, liquidation_*_risk≠high) sale a 0.750.80."
),
)
trades_per_year = col_d.slider(
trades_per_year = col_e.slider(
"Trade / anno (post-filtri)", 20, 200, value=110, step=5,
help=(
"Crypto è 24/7: l'entry cycle gira ogni giorno alle 14:00 UTC "
@@ -702,6 +712,43 @@ def _render_pl_panel(
cons_caps = _profile_caps(strategy_conservativa or strategy_main)
aggr_caps = _profile_caps(strategy_aggressiva)
# Override sizing dai slider (sostituisce le leve cap/trade,
# cap_aggregate, max_concurrent dei yaml).
eur_to_usd = 1.075
cap_pertrade_usd = cap_per_trade_eur * eur_to_usd
cap_aggregate_override = float(cap_per_trade_eur * concurrency_override)
cons_caps = {
**cons_caps,
"cap_pertrade_eur": float(cap_per_trade_eur),
"cap_aggregate_eur": cap_aggregate_override,
"max_concurrent": float(concurrency_override),
}
aggr_caps = {
**aggr_caps,
"cap_pertrade_eur": float(cap_per_trade_eur),
"cap_aggregate_eur": cap_aggregate_override,
"max_concurrent": float(concurrency_override),
}
# Capitale richiesto: Kelly-binding aggregato.
# Per ogni trade slot, kelly × capital ≥ cap_pertrade_usd → capital
# ≥ cap_pertrade_usd / kelly. Per N concorrenti, scala linearmente
# come limite conservativo del notional cumulato.
kelly_cons = cons_caps.get("kelly", 0.13)
kelly_aggr = aggr_caps.get("kelly", 0.13)
capital_cons = int(
cap_pertrade_usd * concurrency_override / max(kelly_cons, 1e-3)
)
capital_aggr = int(
cap_pertrade_usd * concurrency_override / max(kelly_aggr, 1e-3)
)
capital = max(capital_cons, capital_aggr)
cap_col1, cap_col2, cap_col3 = st.columns(3)
cap_col1.metric("📊 Capitale richiesto", f"${capital:,}")
cap_col2.metric(
"💸 Cap aggregato (notional)",
f"${int(cap_pertrade_usd * concurrency_override):,}",
)
cap_col3.metric("🎯 Cap per trade (USD)", f"${int(cap_pertrade_usd):,}")
cons_feats = _detect_features(strategy_conservativa or strategy_main)
aggr_feats = _detect_features(strategy_aggressiva)