fix(TP01): bug look-ahead ffill mixed-TF -> deploy a >=12h (1d), strategia DIFENSIVA
Segnalato: ffill MIXED-TIMEFRAME su barre open-labeled (resample label="left") gonfiava il 4h (~1.60 -> reale ~1.1). Ri-verifica per-SINGOLO-TF leak-free (guard prefix-recompute, leak=0 su 4h/6h/12h/1d): FULL Sh piatto ~1.3, hold-out 2025-26 MIGLIORE a 1d (Sh 0.31 / +3.5% vs buy&hold -39%). Conclusione adottata: NON scendere sotto le 12h (sotto, costi+overfit dominano senza vantaggio). - trend_portfolio.py: canonica PORT LF1d; resample_tf/resample_1d (resample_4h deprecato deploy); docstring con nota look-ahead + natura DIFENSIVA (taglia DD ~6x, non alpha). - paper_trend.py: deploy a 1d (resample_1d, build_bars). 5 test passano. - CLAUDE.md: TP01 ridescritta (>=12h/1d, gotcha ffill mixed-TF, difensiva). - tp01_lowfreq.py + diario 2026-06-19-tp01-lookahead-fix-lf.md. Gotcha: mai ffill/combine mixed-TF su timestamp open-labeled (close propagata indietro = leak). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user