feat(gui): simulazione P/L con effetti dei miglioramenti FDAC + IV-RV
Estende il pannello "💰 P/L atteso" della pagina `📚 Strategia` per applicare gli effetti stimati di IV-RV gate, A (delta dinamico), D (vol-harvest) e F (auto-pause) leggendoli direttamente dai `strategy.*.yaml` di ciascun profilo. - Nuova `_detect_features(strategy)` che ispeziona la config: A → `short_strike.delta_by_dvol` non vuoto D → `exit.vol_harvest_dvol_decrease > 0` F → `auto_pause.enabled` IV → `entry.iv_minus_rv_filter_enabled` - `_compute_pl` accetta ora un dict `features` opzionale e applica: IV: +5 pp win-rate, −25% trade/anno (skip-week aggressivo) A: +1.5 pp win-rate, sl_loss × 0.95 (strike picking migliore) D: 5% trade convertiti da loss a harvest exit (+0.20×credito) F: −8% trade/anno (skip-week dopo streak) - `_render_profile_card` mostra ora: badge "🟢 Miglioramenti attivi" con la lista per profilo, delta vs base in E[trade] e P/L annuo, help con win_rate effettivo / prob_loss / trade/anno. - Checkbox "Applica effetti dei miglioramenti" (default ON) per switchare tra simulazione realistica e formula base. - Nuova mini-tabella "Contributo marginale di ogni feature": per ogni miglioramento mostra ΔP/L annuo e ΔAPR isolando l'effetto del singolo feature, con marker "✅ attiva nel YAML". - Sensibilità win-rate ora applica le feature attive ai due profili. Effetti dichiarati come **stime ex-ante** dalla letteratura short-vol systematic; i valori puntuali (+5 pp win, etc.) andranno calibrati sul dataset accumulato. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user