From c4cd2986a4c0c73902ca6847160f79aca020e097 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 19:50:09 +0000 Subject: [PATCH] feat(gui): aggiunge max drawdown atteso (P99) e tail/gap nei profili MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Due metriche per ciascun profilo nel pannello P/L: - **Max DD attesa (P99)**: streak di stop consecutivi con probabilità ≤ 1% nell'anno (union-bound: N_trade × p_loss^N ≤ 0.01) × perdita stop × contratti × posizioni concorrenti. - **Max DD coda (gap)**: scenario gap notturno in cui il mark salta oltre la copertura long PRIMA che lo stop sia eseguibile — perdita = larghezza intera meno credito iniziale, su tutte le posizioni aperte. Aggiunge anche colonna "Max DD" nella tabella di sensibilità win-rate, così si vede immediatamente il trade-off APR-vs-drawdown al variare del win-rate (da 65% a 82%). Effetto pratico: a default cap=10k, spot=3000, win=0.75, trades=18: - Conservativa: APR ≈ +1.8%, Max DD attesa ≈ −2.2% capitale - Aggressiva: APR ≈ +14%, Max DD attesa ≈ −30% capitale Numeri che rendono molto più tangibile la frase "drawdown scala con lo stesso fattore" del §4-ter del documento. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/cerbero_bite/gui/pages/7_📚_Strategia.py | 74 ++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/src/cerbero_bite/gui/pages/7_📚_Strategia.py b/src/cerbero_bite/gui/pages/7_📚_Strategia.py index 2b9964a..fa57003 100644 --- a/src/cerbero_bite/gui/pages/7_📚_Strategia.py +++ b/src/cerbero_bite/gui/pages/7_📚_Strategia.py @@ -11,6 +11,7 @@ La pagina è di sola lettura: non chiama MCP, non scrive sul DB. from __future__ import annotations +import math import os from dataclasses import dataclass from pathlib import Path @@ -420,6 +421,37 @@ def _compute_pl( annual_pl = trades_per_year * n_per_trade * concurrency * e_trade_net apr = (annual_pl / capital) if capital > 0 else 0.0 + # --- Max drawdown ------------------------------------------------- + # Due metriche distinte: + # + # 1. **Streak atteso (P99)**: lunghezza della peggior sequenza di + # stop consecutivi che ci si aspetta di vedere in un anno con + # probabilità ≤ 1%. Usa l'approssimazione union-bound: + # P(streak ≥ N in N_trade tentativi) ≈ N_trade × p_loss^N + # Imponendo questa quantità ≤ 0.01 e risolvendo per N: + # N = ceil( log(0.01 / N_trade) / log(p_loss) ) + # Drawdown corrispondente = N × stop_loss × contracts × concurrency. + # + # 2. **Tail/gap risk**: scenario "gap notturno" in cui il mark + # salta oltre la copertura long PRIMA che lo stop sia + # eseguibile. La perdita massima reale è la larghezza intera + # dello spread meno il credito iniziale, su tutte le posizioni + # aperte simultaneamente. + if prob_loss > 0 and prob_loss < 1 and trades_per_year > 0: + streak_99 = max( + 1, + int(math.ceil( + math.log(0.01 / trades_per_year) / math.log(prob_loss) + )) if prob_loss < 1 else 1, + ) + else: + streak_99 = 0 + expected_dd_usd = streak_99 * sl_loss * n_per_trade * concurrency + expected_dd_pct = expected_dd_usd / capital if capital > 0 else 0.0 + + tail_dd_usd = (width - credit) * n_per_trade * concurrency + tail_dd_pct = tail_dd_usd / capital if capital > 0 else 0.0 + return { "width": width, "credit": credit, @@ -433,6 +465,12 @@ def _compute_pl( "apr": apr, "fees": fees, "slippage": slippage, + "prob_loss": prob_loss, + "streak_99": float(streak_99), + "expected_dd_usd": expected_dd_usd, + "expected_dd_pct": expected_dd_pct, + "tail_dd_usd": tail_dd_usd, + "tail_dd_pct": tail_dd_pct, } @@ -469,6 +507,34 @@ def _render_profile_card( delta=f"{metrics['apr']:+.1%} APR", ) + cols = st.columns(2) + cols[0].metric( + "Max DD attesa (P99)", + f"−{metrics['expected_dd_usd']:.0f} USD", + delta=f"{-metrics['expected_dd_pct']:+.1%} cap", + delta_color="inverse", + help=( + f"Streak di {int(metrics['streak_99'])} stop consecutivi " + f"(probabilità ≤ 1% nell'anno) × perdita stop " + f"({metrics['sl_loss']:.0f} USD) × contratti × posizioni " + f"concorrenti. È la peggior sequenza che ti aspetti di " + "vedere; il drawdown reale può essere maggiore se i filtri " + "non rilevano un regime change." + ), + ) + cols[1].metric( + "Max DD coda (gap)", + f"−{metrics['tail_dd_usd']:.0f} USD", + delta=f"{-metrics['tail_dd_pct']:+.1%} cap", + delta_color="inverse", + help=( + "Scenario gap notturno: il mark salta oltre la copertura " + "long PRIMA che lo stop sia eseguibile. Perdita = larghezza " + "intera meno credito, su tutte le posizioni aperte. " + "I filtri quant + macro lo riducono ma NON lo annullano." + ), + ) + if metrics["n_per_trade"] == 0: st.warning( "Sizing 0 contratti: capitale insufficiente per i cap di " @@ -581,10 +647,10 @@ def _render_pl_panel( sens_rows.append( { "Win rate": f"{wr:.0%}", - "Conservativa P/L": f"{m_c['annual_pl']:+.0f} USD", - "Conservativa APR": f"{m_c['apr']:+.1%}", - "Aggressiva P/L": f"{m_a['annual_pl']:+.0f} USD", - "Aggressiva APR": f"{m_a['apr']:+.1%}", + "Cons. APR": f"{m_c['apr']:+.1%}", + "Cons. Max DD": f"−{m_c['expected_dd_pct']:.1%}", + "Aggr. APR": f"{m_a['apr']:+.1%}", + "Aggr. Max DD": f"−{m_a['expected_dd_pct']:.1%}", } ) st.table(sens_rows)