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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -420,6 +421,37 @@ def _compute_pl(
|
|||||||
annual_pl = trades_per_year * n_per_trade * concurrency * e_trade_net
|
annual_pl = trades_per_year * n_per_trade * concurrency * e_trade_net
|
||||||
apr = (annual_pl / capital) if capital > 0 else 0.0
|
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 {
|
return {
|
||||||
"width": width,
|
"width": width,
|
||||||
"credit": credit,
|
"credit": credit,
|
||||||
@@ -433,6 +465,12 @@ def _compute_pl(
|
|||||||
"apr": apr,
|
"apr": apr,
|
||||||
"fees": fees,
|
"fees": fees,
|
||||||
"slippage": slippage,
|
"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",
|
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:
|
if metrics["n_per_trade"] == 0:
|
||||||
st.warning(
|
st.warning(
|
||||||
"Sizing 0 contratti: capitale insufficiente per i cap di "
|
"Sizing 0 contratti: capitale insufficiente per i cap di "
|
||||||
@@ -581,10 +647,10 @@ def _render_pl_panel(
|
|||||||
sens_rows.append(
|
sens_rows.append(
|
||||||
{
|
{
|
||||||
"Win rate": f"{wr:.0%}",
|
"Win rate": f"{wr:.0%}",
|
||||||
"Conservativa P/L": f"{m_c['annual_pl']:+.0f} USD",
|
"Cons. APR": f"{m_c['apr']:+.1%}",
|
||||||
"Conservativa APR": f"{m_c['apr']:+.1%}",
|
"Cons. Max DD": f"−{m_c['expected_dd_pct']:.1%}",
|
||||||
"Aggressiva P/L": f"{m_a['annual_pl']:+.0f} USD",
|
"Aggr. APR": f"{m_a['apr']:+.1%}",
|
||||||
"Aggressiva APR": f"{m_a['apr']:+.1%}",
|
"Aggr. Max DD": f"−{m_a['expected_dd_pct']:.1%}",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
st.table(sens_rows)
|
st.table(sens_rows)
|
||||||
|
|||||||
Reference in New Issue
Block a user