diff --git a/src/cerbero_bite/gui/pages/7_📚_Strategia.py b/src/cerbero_bite/gui/pages/7_📚_Strategia.py index cfe1601..16b4f53 100644 --- a/src/cerbero_bite/gui/pages/7_📚_Strategia.py +++ b/src/cerbero_bite/gui/pages/7_📚_Strategia.py @@ -347,6 +347,44 @@ def _profile_caps(strategy: object | None) -> dict[str, float]: return out +def _detect_features(strategy: object | None) -> dict[str, bool]: + """Quali miglioramenti del PR FDAC sono ATTIVI in questa strategia. + + - **A** (delta dinamico): `short_strike.delta_by_dvol` non vuoto. + - **D** (vol-harvest): `exit.vol_harvest_dvol_decrease > 0`. + - **F** (auto-pause): `auto_pause.enabled = true`. + - **IV** (IV-richness gate, dal PR precedente): `entry.iv_minus_rv_filter_enabled`. + """ + feats = {"A": False, "D": False, "F": False, "IV": False} + if strategy is None: + return feats + try: + feats["A"] = bool( + getattr(strategy.structure.short_strike, "delta_by_dvol", []) # type: ignore[attr-defined] + ) + except Exception: + pass + try: + feats["D"] = ( + float(getattr(strategy.exit, "vol_harvest_dvol_decrease", 0)) > 0 # type: ignore[attr-defined] + ) + except Exception: + pass + try: + feats["F"] = bool( + getattr(getattr(strategy, "auto_pause", None), "enabled", False) + ) + except Exception: + pass + try: + feats["IV"] = bool( + getattr(strategy.entry, "iv_minus_rv_filter_enabled", False) # type: ignore[attr-defined] + ) + except Exception: + pass + return feats + + def _compute_pl( caps: dict[str, float], *, @@ -355,13 +393,56 @@ def _compute_pl( win_rate: float, trades_per_year: int, eur_to_usd: float = 1.075, + features: dict[str, bool] | None = None, ) -> dict[str, float]: - """Calcola le metriche P/L per un profilo di sizing.""" + """Calcola le metriche P/L per un profilo di sizing. + + Quando ``features`` è popolato, applica gli effetti stimati dei + miglioramenti del PR FDAC + IV-RV gate: + + - ``IV`` (IV-richness gate, §2.9): +5 pp win-rate, −25% trade/anno. + - ``A`` (delta dinamico, §3.2): +1.5 pp win-rate, sl_loss × 0.95. + - ``D`` (vol-harvest, §7-bis): 5% delle would-be-loss diventano + harvest exit a +0.20 × credito. + - ``F`` (auto-pause, §7-bis): −8% trade/anno (skip-week dopo + streak), e nei calcoli di drawdown atteso il streak_99 è + cappato a lookback_trades=5. + + Effetti **stimati ex-ante** dalla letteratura short-vol systematic; + i valori puntuali andranno calibrati sul dataset accumulato. + """ + feats = features or {} width = caps["width_pct"] * spot credit = caps["credit_ratio"] * width tp_profit = caps["profit_take"] * credit sl_loss = (caps["stop_mult"] - 1.0) * credit + # === Effetti dei miglioramenti ===================================== + win_rate_eff = win_rate + trades_eff = float(trades_per_year) + sl_loss_eff = sl_loss + extra_harvest_ev = 0.0 + prob_harvest = 0.0 + + if feats.get("IV"): + # Skip più aggressivo + qualità migliore: +5 pp win, −25% trade. + win_rate_eff = min(0.95, win_rate_eff + 0.05) + trades_eff *= 0.75 + if feats.get("A"): + # Migliore strike picking → +1.5 pp win-rate; riduzione del + # tail della perdita (5%) per le bande high-DVOL. + win_rate_eff = min(0.95, win_rate_eff + 0.015) + sl_loss_eff *= 0.95 + if feats.get("D"): + # Vol-harvest: ~5% delle entrate intercettate prima dello stop + # con un piccolo profitto (+0.20×credit). Sottrae lo stesso + # volume dalle prob_loss. + prob_harvest = 0.05 + extra_harvest_ev = 0.20 * credit + # F (auto-pause) agisce su streak_99 più sotto, e sul trades_eff. + if feats.get("F"): + trades_eff *= 0.92 + 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 @@ -369,32 +450,31 @@ def _compute_pl( prob_time_stop = 0.07 prob_other_stop = 0.03 - prob_loss = max(0.0, 1.0 - win_rate - prob_time_stop - prob_other_stop) + prob_loss = max( + 0.0, + 1.0 - win_rate_eff - prob_time_stop - prob_other_stop - prob_harvest, + ) avg_time_stop_pl = 0.10 * credit e_trade_gross = ( - win_rate * tp_profit - - prob_loss * sl_loss + win_rate_eff * tp_profit + - prob_loss * sl_loss_eff + prob_time_stop * avg_time_stop_pl + + prob_harvest * extra_harvest_ev ) fees = 0.0003 * spot * 2 slippage = 0.03 * credit e_trade_net = e_trade_gross - fees - slippage - # Multi-posizione concorrente: il P/L scala col numero di posizioni - # aperte simultaneamente (il loop entry crea N trade indipendenti - # quando max_concurrent > 1). Vedi caveat aggressiva.yaml: il - # supporto multi-asset richiede modifiche di codice; questo - # moltiplicatore stima cosa otterresti DOPO. concurrency = max(1.0, caps["max_concurrent"]) - annual_pl = trades_per_year * n_per_trade * concurrency * e_trade_net + annual_pl = trades_eff * n_per_trade * concurrency * e_trade_net apr = (annual_pl / capital) if capital > 0 else 0.0 return { "width": width, "credit": credit, "tp_profit": tp_profit, - "sl_loss": sl_loss, + "sl_loss": sl_loss_eff, "risk_target": risk_target, "n_per_trade": float(n_per_trade), "concurrency": concurrency, @@ -403,6 +483,10 @@ def _compute_pl( "apr": apr, "fees": fees, "slippage": slippage, + "win_rate_eff": win_rate_eff, + "trades_eff": trades_eff, + "prob_loss": prob_loss, + "prob_harvest": prob_harvest, } @@ -411,6 +495,8 @@ def _render_profile_card( caps: dict[str, float], metrics: dict[str, float], badge: str, + features: dict[str, bool] | None = None, + metrics_base: dict[str, float] | None = None, ) -> None: """Rendering di un profilo (conservativo o aggressivo) in una colonna.""" st.markdown(f"### {label} {badge}") @@ -420,23 +506,58 @@ def _render_profile_card( f"max {caps['max_n']:.0f} contratti × " f"{caps['max_concurrent']:.0f} pos. concorrenti" ) + + if features: + active = [k for k, v in features.items() if v] + if active: + st.caption( + "🟢 Miglioramenti attivi: " + + " · ".join( + { + "IV": "**IV-RV gate**", + "A": "**A** delta dinamico", + "D": "**D** vol-harvest", + "F": "**F** auto-pause", + }.get(k, k) + for k in active + ) + ) + else: + st.caption("⚪ Nessun miglioramento attivo (formula base)") + cols = st.columns(2) cols[0].metric("Contratti per trade", f"{metrics['n_per_trade']:.0f}") cols[1].metric("Posizioni concorrenti", f"{metrics['concurrency']:.0f}") cols = st.columns(2) + e_delta = ( + f"{metrics['e_trade_net'] - metrics_base['e_trade_net']:+.1f}" + if metrics_base + else None + ) + pl_delta = ( + f"{metrics['annual_pl'] - metrics_base['annual_pl']:+.0f} USD vs base" + if metrics_base + else f"{metrics['apr']:+.1%} APR" + ) cols[0].metric( "E[trade] netto", f"{metrics['e_trade_net']:+.1f} USD", + delta=e_delta, help=( - f"fees={metrics['fees']:.2f} USD, " - f"slippage={metrics['slippage']:.2f} USD" + f"win_rate effettivo={metrics['win_rate_eff']:.0%}, " + f"prob_loss={metrics['prob_loss']:.0%}, " + f"trade/anno={metrics['trades_eff']:.0f}" ), ) cols[1].metric( "P/L annuo stimato", f"{metrics['annual_pl']:+.0f} USD", - delta=f"{metrics['apr']:+.1%} APR", + delta=f"{metrics['apr']:+.1%} APR" + ( + f" ({metrics['annual_pl'] - metrics_base['annual_pl']:+.0f} vs base)" + if metrics_base + else "" + ), ) if metrics["n_per_trade"] == 0: @@ -481,12 +602,45 @@ def _render_pl_panel( cons_caps = _profile_caps(strategy_conservativa or strategy_main) aggr_caps = _profile_caps(strategy_aggressiva) + cons_feats = _detect_features(strategy_conservativa or strategy_main) + aggr_feats = _detect_features(strategy_aggressiva) + + apply_features = st.checkbox( + "Applica gli effetti dei miglioramenti FDAC + IV-RV gate " + "letti dai due `strategy.*.yaml`", + value=True, + help=( + "Quando ON, ogni colonna applica gli effetti stimati delle " + "feature attive nel rispettivo profilo. OFF = formula base " + "(senza miglioramenti) per confronto pulito." + ), + ) + + feats_cons = cons_feats if apply_features else {} + feats_aggr = aggr_feats if apply_features else {} + + # Calcoli "base" (senza feature) per la delta che mostriamo nel card. + cons_base = _compute_pl( + cons_caps, + capital=capital, + spot=spot, + win_rate=win_rate, + trades_per_year=trades_per_year, + ) + aggr_base = _compute_pl( + aggr_caps, + capital=capital, + spot=spot, + win_rate=win_rate, + trades_per_year=trades_per_year, + ) cons = _compute_pl( cons_caps, capital=capital, spot=spot, win_rate=win_rate, trades_per_year=trades_per_year, + features=feats_cons, ) aggr = _compute_pl( aggr_caps, @@ -494,6 +648,7 @@ def _render_pl_panel( spot=spot, win_rate=win_rate, trades_per_year=trades_per_year, + features=feats_aggr, ) col_cons, col_aggr = st.columns(2) @@ -502,7 +657,9 @@ def _render_pl_panel( "🛡️ Conservativa", cons_caps, cons, - "_(golden config v1.0.0)_", + "_(golden config v1.2.0)_", + features=feats_cons, + metrics_base=cons_base if apply_features and any(feats_cons.values()) else None, ) with col_aggr: _render_profile_card( @@ -510,6 +667,8 @@ def _render_pl_panel( aggr_caps, aggr, "_(deroga §11, richiede paper trading)_", + features=feats_aggr, + metrics_base=aggr_base if apply_features and any(feats_aggr.values()) else None, ) if aggr["annual_pl"] > 0 and cons["annual_pl"] > 0: @@ -530,6 +689,43 @@ def _render_pl_panel( "viable." ) + # === Mini-tabella: contributo marginale di ogni feature ===== + if apply_features and (any(feats_cons.values()) or any(feats_aggr.values())): + st.markdown("**Contributo marginale di ogni feature** (profilo aggressivo)") + contrib_rows = [] + for label, key in [ + ("IV — IV-richness gate", "IV"), + ("A — Delta dinamico", "A"), + ("D — Vol-harvest", "D"), + ("F — Auto-pause", "F"), + ]: + single_feat = {key: True} + m = _compute_pl( + aggr_caps, + capital=capital, + spot=spot, + win_rate=win_rate, + trades_per_year=trades_per_year, + features=single_feat, + ) + delta_pl = m["annual_pl"] - aggr_base["annual_pl"] + delta_apr = m["apr"] - aggr_base["apr"] + active = "✅" if aggr_feats.get(key) else "—" + contrib_rows.append( + { + "Feature": label, + "Attiva nel YAML": active, + "ΔP/L annuo (solo questa)": f"{delta_pl:+.0f} USD", + "ΔAPR": f"{delta_apr:+.1%}", + } + ) + st.table(contrib_rows) + st.caption( + "Ogni riga mostra il contributo del SINGOLO feature (le altre " + "spente). Effetti stimati ex-ante; calibrabili sui dati " + "raccolti via `📐 Calibrazione`." + ) + # Sensibilità win-rate per il profilo aggressivo (più informativo) st.markdown("**Sensibilità al win rate** (profilo aggressivo)") sens_rows = [] @@ -540,6 +736,7 @@ def _render_pl_panel( spot=spot, win_rate=wr, trades_per_year=trades_per_year, + features=feats_aggr, ) m_c = _compute_pl( cons_caps, @@ -547,6 +744,7 @@ def _render_pl_panel( spot=spot, win_rate=wr, trades_per_year=trades_per_year, + features=feats_cons, ) sens_rows.append( {