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:
Adriano Dal Pastro
2026-06-07 17:32:00 +00:00
parent bc0ad3e86f
commit 2a97294d67
3 changed files with 200 additions and 21 deletions
File diff suppressed because one or more lines are too long
+186 -9
View File
@@ -398,6 +398,172 @@ def chart_weights():
return b64(fig) 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 # ----------------------------------------------------------------- HTML
CSS = """ CSS = """
body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;background:#f4f5f7;color:#222} 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)...") print("genero grafici (episodi reali)...")
panel = _daily_panel() 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_w = chart_weights()
g_mr01 = chart_fade("scripts.strategies.MR01_bollinger_fade", "BTC", g_mr01 = chart_fade("scripts.strategies.MR01_bollinger_fade", "BTC",
dict(trend_max=3.0, ema_long=200), mr01_bands, 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, """ 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 <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, 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, """ 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): <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. 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, """ 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 <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 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"), """ 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 <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, """ 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), <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 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, """ 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, <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 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, """ 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 <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&lt;SMA100. Diversificare su 3 (solo se positive), gross 0.45. Gate di regime: tutto cash se BTC&lt;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)", c_pr = card("PR01 — Pairs Reversion (ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL)",
B("pairs", "PAIRS") + sim, """ 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 (|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 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. 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, """ 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), <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&lt;SMA100. Mai un anno negativo nel backtest. Non è un motore di gross 0.30, cash totale se BTC&lt;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. 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, """ 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 <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 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> 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 <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"> html = f"""<!doctype html><html lang="it"><head><meta charset="utf-8">
<title>PythagorasGoal — Strategie attive PORT06</title><style>{CSS}</style></head> <title>PythagorasGoal — Strategie attive PORT06</title><style>{CSS}</style></head>
+4 -2
View File
@@ -51,7 +51,7 @@ def pairs_sim(a, b, tf="1h", n=50, z_in=2.0, z_exit=0.5, max_bars=72,
split = int(N * split_frac) split = int(N * split_frac)
fee = 2 * fee_rt * lev # 2 gambe fee = 2 * fee_rt * lev # 2 gambe
cap = peak = 1000.0; dd = 0.0; last = -1 cap = peak = 1000.0; dd = 0.0; last = -1
trades = wins = 0; rets = []; yearly = {} trades = wins = 0; rets = []; yearly = {}; yearly_n = {}
eq_ts: list = []; eq_v: list = [] eq_ts: list = []; eq_v: list = []
for i in range(n + 1, N - 1): for i in range(n + 1, N - 1):
if i < split or np.isnan(z[i]) or dr[i] > jump_max: if i < split or np.isnan(z[i]) or dr[i] > jump_max:
@@ -81,6 +81,7 @@ def pairs_sim(a, b, tf="1h", n=50, z_in=2.0, z_exit=0.5, max_bars=72,
trades += 1; wins += ret > 0; rets.append(ret * pos); last = j trades += 1; wins += ret > 0; rets.append(ret * pos); last = j
eq_ts.append(ts.iloc[j]); eq_v.append(cap) eq_ts.append(ts.iloc[j]); eq_v.append(cap)
yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100 yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100
yearly_n[ts.iloc[i].year] = yearly_n.get(ts.iloc[i].year, 0) + 1
yrs_span = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1 yrs_span = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1
sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(BARS_YEAR / np.mean([max_bars])) ) if len(rets) > 1 and np.std(rets) > 0 else 0.0 sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(BARS_YEAR / np.mean([max_bars])) ) if len(rets) > 1 and np.std(rets) > 0 else 0.0
# Sharpe annualizzato sul tempo reale: usa rendimenti per-trade scalati alla frequenza media # Sharpe annualizzato sul tempo reale: usa rendimenti per-trade scalati alla frequenza media
@@ -90,7 +91,8 @@ def pairs_sim(a, b, tf="1h", n=50, z_in=2.0, z_exit=0.5, max_bars=72,
ret_tot = (cap / 1000 - 1) * 100 ret_tot = (cap / 1000 - 1) * 100
cagr = ((cap / 1000) ** (1 / yrs_span) - 1) * 100 if cap > 0 else -100 cagr = ((cap / 1000) ** (1 / yrs_span) - 1) * 100 if cap > 0 else -100
return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=ret_tot, cagr=cagr, return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=ret_tot, cagr=cagr,
dd=dd * 100, sharpe=sharpe, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v) dd=dd * 100, sharpe=sharpe, yearly=yearly, yearly_n=yearly_n,
eq_ts=eq_ts, eq_v=eq_v)
def check_no_lookahead(): def check_no_lookahead():