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
|
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(
|
def _compute_pl(
|
||||||
caps: dict[str, float],
|
caps: dict[str, float],
|
||||||
*,
|
*,
|
||||||
@@ -355,13 +393,56 @@ def _compute_pl(
|
|||||||
win_rate: float,
|
win_rate: float,
|
||||||
trades_per_year: int,
|
trades_per_year: int,
|
||||||
eur_to_usd: float = 1.075,
|
eur_to_usd: float = 1.075,
|
||||||
|
features: dict[str, bool] | None = None,
|
||||||
) -> dict[str, float]:
|
) -> 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
|
width = caps["width_pct"] * spot
|
||||||
credit = caps["credit_ratio"] * width
|
credit = caps["credit_ratio"] * width
|
||||||
tp_profit = caps["profit_take"] * credit
|
tp_profit = caps["profit_take"] * credit
|
||||||
sl_loss = (caps["stop_mult"] - 1.0) * 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
|
cap_pertrade_usd = caps["cap_pertrade_eur"] * eur_to_usd
|
||||||
risk_target = min(caps["kelly"] * capital, cap_pertrade_usd)
|
risk_target = min(caps["kelly"] * capital, cap_pertrade_usd)
|
||||||
n_kelly = int(risk_target // width) if width > 0 else 0
|
n_kelly = int(risk_target // width) if width > 0 else 0
|
||||||
@@ -369,32 +450,31 @@ def _compute_pl(
|
|||||||
|
|
||||||
prob_time_stop = 0.07
|
prob_time_stop = 0.07
|
||||||
prob_other_stop = 0.03
|
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
|
avg_time_stop_pl = 0.10 * credit
|
||||||
|
|
||||||
e_trade_gross = (
|
e_trade_gross = (
|
||||||
win_rate * tp_profit
|
win_rate_eff * tp_profit
|
||||||
- prob_loss * sl_loss
|
- prob_loss * sl_loss_eff
|
||||||
+ prob_time_stop * avg_time_stop_pl
|
+ prob_time_stop * avg_time_stop_pl
|
||||||
|
+ prob_harvest * extra_harvest_ev
|
||||||
)
|
)
|
||||||
fees = 0.0003 * spot * 2
|
fees = 0.0003 * spot * 2
|
||||||
slippage = 0.03 * credit
|
slippage = 0.03 * credit
|
||||||
e_trade_net = e_trade_gross - fees - slippage
|
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"])
|
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
|
apr = (annual_pl / capital) if capital > 0 else 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"width": width,
|
"width": width,
|
||||||
"credit": credit,
|
"credit": credit,
|
||||||
"tp_profit": tp_profit,
|
"tp_profit": tp_profit,
|
||||||
"sl_loss": sl_loss,
|
"sl_loss": sl_loss_eff,
|
||||||
"risk_target": risk_target,
|
"risk_target": risk_target,
|
||||||
"n_per_trade": float(n_per_trade),
|
"n_per_trade": float(n_per_trade),
|
||||||
"concurrency": concurrency,
|
"concurrency": concurrency,
|
||||||
@@ -403,6 +483,10 @@ def _compute_pl(
|
|||||||
"apr": apr,
|
"apr": apr,
|
||||||
"fees": fees,
|
"fees": fees,
|
||||||
"slippage": slippage,
|
"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],
|
caps: dict[str, float],
|
||||||
metrics: dict[str, float],
|
metrics: dict[str, float],
|
||||||
badge: str,
|
badge: str,
|
||||||
|
features: dict[str, bool] | None = None,
|
||||||
|
metrics_base: dict[str, float] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Rendering di un profilo (conservativo o aggressivo) in una colonna."""
|
"""Rendering di un profilo (conservativo o aggressivo) in una colonna."""
|
||||||
st.markdown(f"### {label} {badge}")
|
st.markdown(f"### {label} {badge}")
|
||||||
@@ -420,23 +506,58 @@ def _render_profile_card(
|
|||||||
f"max {caps['max_n']:.0f} contratti × "
|
f"max {caps['max_n']:.0f} contratti × "
|
||||||
f"{caps['max_concurrent']:.0f} pos. concorrenti"
|
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 = st.columns(2)
|
||||||
cols[0].metric("Contratti per trade", f"{metrics['n_per_trade']:.0f}")
|
cols[0].metric("Contratti per trade", f"{metrics['n_per_trade']:.0f}")
|
||||||
cols[1].metric("Posizioni concorrenti", f"{metrics['concurrency']:.0f}")
|
cols[1].metric("Posizioni concorrenti", f"{metrics['concurrency']:.0f}")
|
||||||
|
|
||||||
cols = st.columns(2)
|
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(
|
cols[0].metric(
|
||||||
"E[trade] netto",
|
"E[trade] netto",
|
||||||
f"{metrics['e_trade_net']:+.1f} USD",
|
f"{metrics['e_trade_net']:+.1f} USD",
|
||||||
|
delta=e_delta,
|
||||||
help=(
|
help=(
|
||||||
f"fees={metrics['fees']:.2f} USD, "
|
f"win_rate effettivo={metrics['win_rate_eff']:.0%}, "
|
||||||
f"slippage={metrics['slippage']:.2f} USD"
|
f"prob_loss={metrics['prob_loss']:.0%}, "
|
||||||
|
f"trade/anno={metrics['trades_eff']:.0f}"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
cols[1].metric(
|
cols[1].metric(
|
||||||
"P/L annuo stimato",
|
"P/L annuo stimato",
|
||||||
f"{metrics['annual_pl']:+.0f} USD",
|
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:
|
if metrics["n_per_trade"] == 0:
|
||||||
@@ -481,12 +602,45 @@ def _render_pl_panel(
|
|||||||
|
|
||||||
cons_caps = _profile_caps(strategy_conservativa or strategy_main)
|
cons_caps = _profile_caps(strategy_conservativa or strategy_main)
|
||||||
aggr_caps = _profile_caps(strategy_aggressiva)
|
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 = _compute_pl(
|
||||||
cons_caps,
|
cons_caps,
|
||||||
capital=capital,
|
capital=capital,
|
||||||
spot=spot,
|
spot=spot,
|
||||||
win_rate=win_rate,
|
win_rate=win_rate,
|
||||||
trades_per_year=trades_per_year,
|
trades_per_year=trades_per_year,
|
||||||
|
features=feats_cons,
|
||||||
)
|
)
|
||||||
aggr = _compute_pl(
|
aggr = _compute_pl(
|
||||||
aggr_caps,
|
aggr_caps,
|
||||||
@@ -494,6 +648,7 @@ def _render_pl_panel(
|
|||||||
spot=spot,
|
spot=spot,
|
||||||
win_rate=win_rate,
|
win_rate=win_rate,
|
||||||
trades_per_year=trades_per_year,
|
trades_per_year=trades_per_year,
|
||||||
|
features=feats_aggr,
|
||||||
)
|
)
|
||||||
|
|
||||||
col_cons, col_aggr = st.columns(2)
|
col_cons, col_aggr = st.columns(2)
|
||||||
@@ -502,7 +657,9 @@ def _render_pl_panel(
|
|||||||
"🛡️ Conservativa",
|
"🛡️ Conservativa",
|
||||||
cons_caps,
|
cons_caps,
|
||||||
cons,
|
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:
|
with col_aggr:
|
||||||
_render_profile_card(
|
_render_profile_card(
|
||||||
@@ -510,6 +667,8 @@ def _render_pl_panel(
|
|||||||
aggr_caps,
|
aggr_caps,
|
||||||
aggr,
|
aggr,
|
||||||
"_(deroga §11, richiede paper trading)_",
|
"_(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:
|
if aggr["annual_pl"] > 0 and cons["annual_pl"] > 0:
|
||||||
@@ -530,6 +689,43 @@ def _render_pl_panel(
|
|||||||
"viable."
|
"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)
|
# Sensibilità win-rate per il profilo aggressivo (più informativo)
|
||||||
st.markdown("**Sensibilità al win rate** (profilo aggressivo)")
|
st.markdown("**Sensibilità al win rate** (profilo aggressivo)")
|
||||||
sens_rows = []
|
sens_rows = []
|
||||||
@@ -540,6 +736,7 @@ def _render_pl_panel(
|
|||||||
spot=spot,
|
spot=spot,
|
||||||
win_rate=wr,
|
win_rate=wr,
|
||||||
trades_per_year=trades_per_year,
|
trades_per_year=trades_per_year,
|
||||||
|
features=feats_aggr,
|
||||||
)
|
)
|
||||||
m_c = _compute_pl(
|
m_c = _compute_pl(
|
||||||
cons_caps,
|
cons_caps,
|
||||||
@@ -547,6 +744,7 @@ def _render_pl_panel(
|
|||||||
spot=spot,
|
spot=spot,
|
||||||
win_rate=wr,
|
win_rate=wr,
|
||||||
trades_per_year=trades_per_year,
|
trades_per_year=trades_per_year,
|
||||||
|
features=feats_cons,
|
||||||
)
|
)
|
||||||
sens_rows.append(
|
sens_rows.append(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user