feat(gui): aggiunge max drawdown atteso (P99) e tail/gap nei profili
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user