"""TP01 a BASSA FREQUENZA (>=12h) — ri-verifica dopo il bug look-ahead ffill-mixed-TF. L'utente/agente ha trovato un look-ahead (ffill mixed-timeframe su barre open-labeled) che gonfiava il 4h (~1.60 -> reale ~1.1) e ha concluso: NON scendere sotto le 12h (costi+overfit dominano). Qui ricalcolo TP01 in modo PULITO per singolo TF (barre discrete, posizione shiftata +1, NESSUN ffill/combine mixed-TF) su 4h/12h/1d, con un GUARD di causalita' esplicito sulla serie resamplata (ricalcolo su prefisso). Fee 0.10% RT, hold-out 2025-26 bloccato. uv run python scripts/analysis/tp01_lowfreq.py """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np import pandas as pd from src.data.downloader import load_data from src.strategies.trend_portfolio import TrendPortfolio, simple_returns, CANONICAL HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") def resample_tf(df_1h, rule): g = df_1h.copy() g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True) out = g.resample(rule, label="left", closed="left").agg( {"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"]) out["datetime"] = out.index return out.reset_index(drop=True) def sleeve_net(df, tp): """Per-barra netto di uno sleeve: posizione decisa a close[i-1], tenuta in i (causale, no ffill).""" r = simple_returns(df["close"].values.astype(float)) tgt = tp.target_series(df) held = np.zeros(len(tgt)); held[1:] = tgt[:-1] net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 return np.clip(net, -0.99, None) def causality_ok(df, tp, k=10): """Ricalcola target_series su prefissi e verifica che tgt[i] non cambi (no look-ahead).""" full = tp.target_series(df); n = len(df) rng = np.random.default_rng(0); bad = 0 for i in rng.integers(int(n * 0.6), n - 1, size=k): p = tp.target_series(df.iloc[:i + 1].copy()) if len(p) != i + 1 or not np.isclose(np.nan_to_num(p[i]), np.nan_to_num(full[i]), atol=1e-9): bad += 1 return bad def met(rr, idx): rr = rr[np.isfinite(rr)] if len(rr) < 2 or np.std(rr) == 0: return dict(sh=0, ret=0, dd=0, n=len(rr)) bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median() eq = np.cumprod(1 + rr); pk = np.maximum.accumulate(eq) return dict(sh=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)), ret=float(eq[-1] - 1), dd=float(np.max((pk - eq) / pk)), n=len(rr)) def main(): print("=" * 92) print(" TP01 RI-VERIFICA BASSA FREQUENZA — calcolo pulito per-TF (no ffill mixed-TF) | fee 0.10% RT") print("=" * 92) tp = TrendPortfolio(**CANONICAL) print(f" {'TF':<5s}{'leak':>6s}{'FULL Sh':>9s}{'FULL ret':>10s}{'FULL DD':>9s}{'HOLD Sh':>9s}{'HOLD ret':>10s}{'HOLD DD':>9s}") for tf, rule in [("4h", "4h"), ("6h", "6h"), ("12h", "12h"), ("1d", "1D")]: series = {}; leak = 0 for a in ("BTC", "ETH"): df = resample_tf(load_data(a, "1h"), rule) leak += causality_ok(df, tp) series[a] = pd.Series(sleeve_net(df, tp), index=pd.to_datetime(df["datetime"])) J = pd.concat(series, axis=1, join="inner").fillna(0.0) combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values idx = J.index; ho = idx >= HOLDOUT f = met(combo, idx); h = met(combo[ho], idx[ho]) print(f" {tf:<5s}{leak:>6d}{f['sh']:>9.2f}{f['ret']*100:>+9.0f}%{f['dd']*100:>8.1f}%" f"{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%{h['dd']*100:>8.1f}%") # buy&hold 50/50 a 1d come riferimento hold-out bh = {} for a in ("BTC", "ETH"): df = resample_tf(load_data(a, "1h"), "1D") bh[a] = pd.Series(simple_returns(df["close"].values.astype(float)), index=pd.to_datetime(df["datetime"])) Jb = pd.concat(bh, axis=1, join="inner").fillna(0.0) cb = 0.5 * Jb["BTC"].values + 0.5 * Jb["ETH"].values; ix = Jb.index; ho = ix >= HOLDOUT bhf = met(cb, ix); bhh = met(cb[ho], ix[ho]) print(f"\n buy&hold 50/50 (1d): FULL Sh {bhf['sh']:.2f} ret {bhf['ret']*100:+.0f}% DD {bhf['dd']*100:.0f}%" f" | HOLD-OUT Sh {bhh['sh']:.2f} ret {bhh['ret']*100:+.0f}% DD {bhh['dd']*100:.0f}%") print("\n (leak=0 = nessun look-ahead nel calcolo per-TF. Confronta con la tesi: >=12h trustworthy.)") if __name__ == "__main__": main()