docs: statistiche per anno (trd/PnL%/maxDD) per strategia e mercato nel doc HTML
Ogni card ora include la tabella anno x mercato: - fade MR01/02/07 (BTC+ETH) e DIP01: calcolate col PATH LIVE (EXIT-16 + trend 3.0), trades/PnL per anno di entry, DD per anno su equity compounding pos 0.15 + DD totale - PR01: matrice anno x 5 coppie, cella = PnL% (n trade, DD anno) + riga TOT (pairs_sim espone ora anche yearly_n, modifica non-breaking) - TR01/ROT02/TSM01: ret%/DD% per anno dall'equity canonica daily 2021+ - SH01: per anno dal walk-forward EXPANDING (regime validato e ora live) Nota di convenzione su ogni tabella (leva 3x test vs 2x live, fee incluse) + caveat: finestra canonica dal 2021, anni 2018-2020 mostrati per onesta' storica. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -398,6 +398,172 @@ def chart_weights():
|
||||
return b64(fig)
|
||||
|
||||
|
||||
# ------------------------------------------------------- statistiche per anno
|
||||
POS_T, LEV_T, FEE_T = 0.15, 3.0, 0.001 # convenzione TEST (canonica)
|
||||
|
||||
|
||||
def yearly_stats(trades, ts):
|
||||
"""trades [(i, j, ret_netto_leveraged)] -> ({anno: n/pnl/dd}, dd_totale).
|
||||
n e PnL per anno di ENTRY (Σ rendimenti netti per trade ×100); DD per anno
|
||||
sul path di equity compounding (pos 0.15), peak resettato a inizio anno."""
|
||||
years: dict[int, dict] = {}
|
||||
cap, peak, tot_peak, tot_dd = 1000.0, 1000.0, 1000.0, 0.0
|
||||
cur = None
|
||||
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||
ey, xy = ts.iloc[i].year, ts.iloc[j].year
|
||||
d = years.setdefault(ey, {"n": 0, "pnl": 0.0, "dd": 0.0})
|
||||
d["n"] += 1
|
||||
d["pnl"] += ret * 100
|
||||
cap = max(cap + cap * POS_T * ret, 10.0)
|
||||
if cur != xy:
|
||||
cur, peak = xy, cap
|
||||
peak = max(peak, cap)
|
||||
tot_peak = max(tot_peak, cap)
|
||||
tot_dd = max(tot_dd, (tot_peak - cap) / tot_peak * 100)
|
||||
dx = years.setdefault(xy, {"n": 0, "pnl": 0.0, "dd": 0.0})
|
||||
dx["dd"] = max(dx["dd"], (peak - cap) / peak * 100)
|
||||
return years, tot_dd
|
||||
|
||||
|
||||
def equity_yearly(eq: pd.Series):
|
||||
"""Equity giornaliera -> {anno: pnl(ret% anno)/dd}, dd_totale (per i multi-asset)."""
|
||||
out = {}
|
||||
for y, g in eq.groupby(eq.index.year):
|
||||
pk = g.cummax()
|
||||
out[int(y)] = {"n": None, "pnl": (g.iloc[-1] / g.iloc[0] - 1) * 100,
|
||||
"dd": float(((pk - g) / pk).max() * 100)}
|
||||
pk = eq.cummax()
|
||||
return out, float(((pk - eq) / pk).max() * 100)
|
||||
|
||||
|
||||
def yearly_table(per_market: dict, note="") -> str:
|
||||
"""{mercato: (years_dict, tot_dd)} -> tabella HTML anno × (trd, PnL%, DD%)."""
|
||||
yrs = sorted({y for ym, _ in per_market.values() for y in ym})
|
||||
head = "".join(f'<th colspan="3">{m}</th>' for m in per_market)
|
||||
sub = "".join("<th>trd</th><th>PnL%</th><th>DD%</th>" for _ in per_market)
|
||||
rows = []
|
||||
for y in yrs:
|
||||
cells = ""
|
||||
for ym, _ in per_market.values():
|
||||
d = ym.get(y)
|
||||
if d:
|
||||
n = "—" if d["n"] is None else d["n"]
|
||||
cells += f"<td>{n}</td><td>{d['pnl']:+.0f}</td><td>{d['dd']:.0f}</td>"
|
||||
else:
|
||||
cells += "<td>—</td><td>—</td><td>—</td>"
|
||||
rows.append(f"<tr><td><b>{y}</b></td>{cells}</tr>")
|
||||
cells = ""
|
||||
for ym, tot_dd in per_market.values():
|
||||
ns = [d["n"] for d in ym.values() if d["n"] is not None]
|
||||
n = sum(ns) if ns else "—"
|
||||
pnl = sum(d["pnl"] for d in ym.values())
|
||||
cells += f"<td><b>{n}</b></td><td><b>{pnl:+.0f}</b></td><td><b>{tot_dd:.0f}</b></td>"
|
||||
rows.append(f'<tr style="background:#f0f2f5"><td><b>TOT</b></td>{cells}</tr>')
|
||||
base = ("PnL = Σ rendimenti netti per trade (%, leva 3x, fee 0.10% RT, convenzione test; "
|
||||
"il live gira a leva 2x); DD = max drawdown nell'anno (equity compounding pos 0.15); "
|
||||
"riga TOT: DD dell'intero periodo. NB: la finestra canonica di valutazione del "
|
||||
"portafoglio parte dal <b>2021</b> — gli anni 2018-2020 (regime microstrutturale "
|
||||
"diverso, spesso negativi) sono mostrati per onestà storica.")
|
||||
return (f'<table><tr><th rowspan="2">anno</th>{head}</tr><tr>{sub}</tr>'
|
||||
+ "".join(rows) + f'</table><p class="sub">{note or base}</p>')
|
||||
|
||||
|
||||
def stats_fades():
|
||||
"""Per-anno delle 3 fade × BTC/ETH col PATH LIVE (EXIT-16 + trend 3.0)."""
|
||||
from scripts.analysis.risk_management import strats_for
|
||||
from scripts.analysis.trendmax_port06_impact import build_trades_variant
|
||||
out = {}
|
||||
for asset in ("BTC", "ETH"):
|
||||
df = load_data(asset, "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
for nm, (fn, params) in strats_for(asset).items():
|
||||
tr = build_trades_variant(fn(df, **params), df, mode="exit16", trend_max=3.0)
|
||||
out.setdefault(nm, {})[asset] = yearly_stats(tr, ts)
|
||||
return out
|
||||
|
||||
|
||||
def stats_dip():
|
||||
from scripts.analysis.dip01_exit16_impact import dip_entries, dip_trades
|
||||
df = load_data("BTC", "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
tr = dip_trades(dip_entries(df), df, "exit16")
|
||||
return {"BTC": yearly_stats(tr, ts)}
|
||||
|
||||
|
||||
def stats_pairs():
|
||||
"""Tabella compatta: per anno 'PnL% (n)' per coppia + righe TOT/DD."""
|
||||
from scripts.analysis.pairs_research import pairs_sim
|
||||
from scripts.strategies.PR01_pairs_reversion import PAIRS as PAIRS_CFG
|
||||
cols, data = [], {}
|
||||
for a, b, p in PAIRS_CFG:
|
||||
r = pairs_sim(a, b, **p)
|
||||
tag = f"{a}/{b}"
|
||||
cols.append(tag)
|
||||
s = pd.Series(r["eq_v"], index=pd.to_datetime(r["eq_ts"], utc=True))
|
||||
ydd = {int(y): float(((g.cummax() - g) / g.cummax()).max() * 100)
|
||||
for y, g in s.groupby(s.index.year)}
|
||||
data[tag] = dict(yearly=r["yearly"], n=r["yearly_n"], dd=r["dd"],
|
||||
trades=r["trades"], ydd=ydd)
|
||||
yrs = sorted({y for d in data.values() for y in d["yearly"]})
|
||||
rows = []
|
||||
for y in yrs:
|
||||
cells = "".join(
|
||||
(f"<td>{data[c]['yearly'][y]:+.0f} <span class='sub'>({data[c]['n'].get(y,0)}"
|
||||
f", dd {data[c]['ydd'].get(y,0):.0f})</span></td>")
|
||||
if y in data[c]["yearly"] else "<td>—</td>" for c in cols)
|
||||
rows.append(f"<tr><td><b>{y}</b></td>{cells}</tr>")
|
||||
tot = "".join(f"<td><b>{sum(data[c]['yearly'].values()):+.0f}</b> "
|
||||
f"<span class='sub'>({data[c]['trades']}, dd {data[c]['dd']:.0f})</span></td>"
|
||||
for c in cols)
|
||||
rows.append(f'<tr style="background:#f0f2f5"><td><b>TOT</b></td>{tot}</tr>')
|
||||
head = "".join(f"<th>{c}</th>" for c in cols)
|
||||
return (f'<table><tr><th>anno</th>{head}</tr>' + "".join(rows) + "</table>"
|
||||
'<p class="sub">cella: PnL% anno (n trade, max DD% anno) — Σ rendimenti netti '
|
||||
"per trade, fee 0.20% RT/coppia (2 gambe), leva 3x convenzione test. NB: la "
|
||||
"finestra canonica del portafoglio parte dal <b>2021</b>; gli anni precedenti "
|
||||
"(spesso negativi) sono mostrati per onestà storica.</p>")
|
||||
|
||||
|
||||
def stats_multi():
|
||||
"""TR01/ROT02/TSM01: per-anno ret/DD dall'equity giornaliera canonica (2021+)."""
|
||||
from scripts.analysis.combine_portfolio import IDX
|
||||
from scripts.analysis.honest_improve2 import _tr_basket_daily, _rot_daily_equity
|
||||
from scripts.analysis.tsmom_research import tsmom_sim
|
||||
from scripts.analysis.report_families import daily_from
|
||||
tr = _tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX)
|
||||
rot = _rot_daily_equity(IDX)
|
||||
t = tsmom_sim()
|
||||
tsm = daily_from(t["eq_ts"], t["eq_v"])
|
||||
note = ("PnL = ritorno % dell'anno dall'equity giornaliera canonica (2021-2026, leva 3x "
|
||||
"convenzione test); DD = max drawdown nell'anno; trd non applicabile (ribilancio "
|
||||
"giornaliero del book).")
|
||||
return (yearly_table({"TR01 (paniere 5)": equity_yearly(tr)}, note),
|
||||
yearly_table({"ROT02 (universo 8)": equity_yearly(rot)}, note),
|
||||
yearly_table({"TSM01 (universo 8)": equity_yearly(tsm)}, note))
|
||||
|
||||
|
||||
def stats_sh01():
|
||||
"""SH01 per-anno dal walk-forward EXPANDING (il regime validato e ora live)."""
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries
|
||||
out = {}
|
||||
for asset in ("BTC", "ETH"):
|
||||
df = load_data(asset, "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
c = df["close"].values
|
||||
ents = ml_wf_entries(df, W=24, H=12, model="logit", thresh=0.58)
|
||||
tr, last = [], -1
|
||||
for e in ents:
|
||||
i, d, mb = e["i"], e["d"], e["max_bars"]
|
||||
j = min(i + mb, len(c) - 1)
|
||||
if i <= last or j <= i:
|
||||
continue
|
||||
ret = (c[j] - c[i]) / c[i] * d * LEV_T - FEE_T * LEV_T
|
||||
tr.append((i, j, ret))
|
||||
last = j
|
||||
out[asset] = yearly_stats(tr, ts)
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- HTML
|
||||
CSS = """
|
||||
body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;background:#f4f5f7;color:#222}
|
||||
@@ -433,6 +599,14 @@ def main():
|
||||
print("genero grafici (episodi reali)...")
|
||||
panel = _daily_panel()
|
||||
|
||||
print("calcolo statistiche per anno (engine canonici/live-path)...")
|
||||
st_fade = stats_fades()
|
||||
st_dip = stats_dip()
|
||||
t_pairs = stats_pairs()
|
||||
t_tr, t_rot, t_tsm = stats_multi()
|
||||
print(" SH01 walk-forward expanding (il piu' lento)...")
|
||||
st_sh = stats_sh01()
|
||||
|
||||
g_w = chart_weights()
|
||||
g_mr01 = chart_fade("scripts.strategies.MR01_bollinger_fade", "BTC",
|
||||
dict(trend_max=3.0, ema_long=200), mr01_bands,
|
||||
@@ -460,17 +634,20 @@ def main():
|
||||
c_mr01 = card("MR01 — Bollinger Fade (BTC, ETH)", B("fade", "FADE") + real, """
|
||||
<p>Quando il <b>close chiude fuori dalla banda</b> ±2.5σ attorno alla SMA50, entra CONTRO il
|
||||
movimento (short sopra, long sotto). TP alla media (il prezzo "torna a casa"), SL a 2·ATR,
|
||||
time-limit 24 barre. Edge OOS validato: BTC +201% / ETH +1238% (fee incluse).</p>""", g_mr01)
|
||||
time-limit 24 barre. Edge OOS validato: BTC +201% / ETH +1238% (fee incluse).</p>""", g_mr01,
|
||||
yearly_table({"BTC": st_fade["MR01"]["BTC"], "ETH": st_fade["MR01"]["ETH"]}))
|
||||
|
||||
c_mr02 = card("MR02 — Donchian Fade (BTC, ETH)", B("fade", "FADE") + real, """
|
||||
<p>Fada la <b>rottura degli estremi del canale</b> Donchian a 20 barre (max/min recenti):
|
||||
short sulla rottura del massimo, long sulla rottura del minimo. TP al centro del canale.
|
||||
Stessa tesi di MR01 con trigger diverso → si combinano bene.</p>""", g_mr02)
|
||||
Stessa tesi di MR01 con trigger diverso → si combinano bene.</p>""", g_mr02,
|
||||
yearly_table({"BTC": st_fade["MR02"]["BTC"], "ETH": st_fade["MR02"]["ETH"]}))
|
||||
|
||||
c_mr07 = card("MR07 — Return Reversal (BTC, ETH)", B("fade", "FADE") + real, """
|
||||
<p>Guarda il <b>rendimento della singola barra</b>: se lo z-score supera ±3.5 (movimento
|
||||
estremo in un'ora), fada il movimento. Exit in multipli di ATR. È la fade più selettiva
|
||||
(esposizione ~8% del tempo).</p>""", g_mr07)
|
||||
(esposizione ~8% del tempo).</p>""", g_mr07,
|
||||
yearly_table({"BTC": st_fade["MR07"]["BTC"], "ETH": st_fade["MR07"]["ETH"]}))
|
||||
|
||||
c_e16 = card("EXIT-16 — perché lo stop scatta solo sul CLOSE", B("fade", "MECCANISMO COMUNE"), """
|
||||
<p>Scoperta chiave della ricerca exit (34 agenti, 23 famiglie): <b>gli stop intrabar da wick
|
||||
@@ -482,17 +659,17 @@ portafoglio: OOS Sharpe 8.82→10.06. Esteso oggi anche a DIP01 (grid 36/36).</p
|
||||
c_dip = card("DIP01 — Dip Buy (BTC)", B("honest", "HONEST") + real, """
|
||||
<p>Compra il <b>dip</b>: quando lo z-score del prezzo incrocia sotto −2.5 (sell-off rapido),
|
||||
entra long. TP alla SMA50, EXIT-16 sul SL, max 24 barre. È l'unico sleeve BTC con round-trip
|
||||
reali su Deribit testnet (TP limit resting + disaster-stop a −30% sul book).</p>""", g_dip)
|
||||
reali su Deribit testnet (TP limit resting + disaster-stop a −30% sul book).</p>""", g_dip, yearly_table(st_dip))
|
||||
|
||||
c_tr = card("TR01 — Basket Trend (4h)", B("honest", "HONEST") + sim, """
|
||||
<p><b>Trend-following difensivo</b>: long quando EMA20>EMA100 sulle 4 ore, flat altrimenti,
|
||||
su un paniere equal-weight di 5 asset (BNB, BTC, DOGE, SOL, XRP). Cattura i trend lunghi che
|
||||
le fade per costruzione non prendono. Valuta solo barre 4h COMPLETE.</p>""", g_tr)
|
||||
le fade per costruzione non prendono. Valuta solo barre 4h COMPLETE.</p>""", g_tr, t_tr)
|
||||
|
||||
c_rot = card("ROT02 — Dual Momentum (1d)", B("honest", "HONEST") + sim, """
|
||||
<p><b>Rotazione</b>: ogni giorno ordina 8 crypto per momentum a 60 giorni e tiene le top-3
|
||||
(solo se positive), gross 0.45. Gate di regime: tutto cash se BTC<SMA100. Diversificare su 3
|
||||
asset invece di 2 ha quasi dimezzato il DD (40%→26%) alzando il ritorno.</p>""", g_rot)
|
||||
asset invece di 2 ha quasi dimezzato il DD (40%→26%) alzando il ritorno.</p>""", g_rot, t_rot)
|
||||
|
||||
c_pr = card("PR01 — Pairs Reversion (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL)",
|
||||
B("pairs", "PAIRS") + sim, """
|
||||
@@ -500,13 +677,13 @@ asset invece di 2 ha quasi dimezzato il DD (40%→26%) alzando il ritorno.</p>""
|
||||
(|z| del log-ratio ≥ 2), compra la gamba debole e shorta la forte; chiude quando il rapporto
|
||||
rientra (|z| ≤ 0.75) o dopo 72 barre. Config <b>universale</b> per tutte le coppie (niente tuning
|
||||
per-coppia = anti-overfit). Correlazione col mercato ~0.05: rende anche quando il mercato è fermo.
|
||||
Fee su 2 gambe. Senza stop per design → position size ridotto a 0.20 (esposizione ≈ validato).</p>""", g_pr)
|
||||
Fee su 2 gambe. Senza stop per design → position size ridotto a 0.20 (esposizione ≈ validato).</p>""", g_pr, t_pairs)
|
||||
|
||||
c_tsm = card("TSM01 — TSMOM (1d)", B("tsm", "TSM") + sim, """
|
||||
<p>Long sugli asset con <b>consenso pieno</b> di momentum su 3 orizzonti (3/6/12 mesi),
|
||||
gross 0.30, cash totale se BTC<SMA100. Mai un anno negativo nel backtest. Non è un motore di
|
||||
ritorno: è il <b>diversificatore</b> che lavora nei regimi in cui le fade soffrono.
|
||||
Attualmente flat by-design (risk-off).</p>""", g_tsm)
|
||||
Attualmente flat by-design (risk-off).</p>""", g_tsm, t_tsm)
|
||||
|
||||
c_sh = card("SH01 — Shape-ML (BTC, ETH)", B("shape", "SHAPE") + sim, """
|
||||
<p>Una <b>LogisticRegression</b> legge 17 feature della <i>forma</i> delle ultime 24 barre e
|
||||
@@ -515,7 +692,7 @@ orizzonte. Training <b>walk-forward causale</b> (mai dati futuri). Win-rate ~50%
|
||||
nell'asimmetria, non nella frequenza. <b>Senza stop-loss by design</b> (ogni stop testato rompe
|
||||
l'edge): la coda si gestisce dimezzando il peso della famiglia (cap 5.88%).</p>
|
||||
<p class='sub'>Fix di oggi (punto-10): il training live usa la storia COMPLETA dal parquet
|
||||
locale (il regime corto a 365g non era robusto: trade-rate 22% vs 10% validato).</p>""", g_sh)
|
||||
locale (il regime corto a 365g non era robusto: trade-rate 22% vs 10% validato).</p>""", g_sh, yearly_table(st_sh))
|
||||
|
||||
html = f"""<!doctype html><html lang="it"><head><meta charset="utf-8">
|
||||
<title>PythagorasGoal — Strategie attive PORT06</title><style>{CSS}</style></head>
|
||||
|
||||
Reference in New Issue
Block a user