d152941360
Integra il lavoro della linea di ricerca parallela (AdrianoDev), verificato indipendentemente
col mio gauntlet onesto (regge il hold-out 2025-26 su entrambi gli asset, plateau 1h/4h/1d):
- src/strategies/trend_portfolio.py TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH)
- src/backtest/harness.py harness onesto (load + backtest_signals no-leakage + OOS)
- scripts/research/track{A,B,C,D,E}_*.py + trackD_timing.py (le 5 track della ricerca)
- scripts/live/paper_trend.py paper trader forward-only di TP01 (no esecuzione reale)
- tests/test_trend_portfolio.py (5 test, passano) + 6 diari trackA-E + synthesis
- CLAUDE.md aggiornato con l'esito ricerca (TP01 vincente, mean-rev morto, onesta su €50/g)
Squash (non merge) per NON portare in git i ~68MB di data/_feed_backup/*.bak che il branch
aveva committato per errore: esclusi + data/_feed_backup/ e data/paper_trend/ ora gitignorati.
Storia granulare del branch conservata sul ref origin/strategy-research-2026-06.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""Test della strategia vincente TP01 (trend portfolio) e del loop paper."""
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from src.backtest.harness import load
|
|
from src.strategies.trend_portfolio import (
|
|
TrendPortfolio, CANONICAL, resample_4h, simple_returns, tsmom_blend)
|
|
|
|
|
|
def _dfs():
|
|
return {a: resample_4h(load(a, "1h")) for a in ("BTC", "ETH")}
|
|
|
|
|
|
def test_no_lookahead_target_is_causal():
|
|
"""target_series[:k] non deve cambiare se aggiungo barre future."""
|
|
df = resample_4h(load("BTC", "1h"))
|
|
tp = TrendPortfolio(**CANONICAL)
|
|
full = tp.target_series(df)
|
|
k = len(df) - 500
|
|
partial = tp.target_series(df.iloc[:k].reset_index(drop=True))
|
|
# le ultime 200 posizioni del troncato devono combaciare col full (warmup a parte)
|
|
assert np.allclose(full[k - 200:k], partial[-200:], atol=1e-9)
|
|
|
|
|
|
def test_canonical_backtest_is_profitable_and_robust():
|
|
tp = TrendPortfolio(**CANONICAL)
|
|
r = tp.backtest_portfolio(_dfs())
|
|
assert r["cagr"] > 0.10, f"CAGR troppo basso: {r['cagr']}"
|
|
assert r["sharpe"] > 1.1, f"Sharpe troppo basso: {r['sharpe']}"
|
|
assert r["max_dd"] < 0.25, f"maxDD troppo alto: {r['max_dd']}"
|
|
# ogni anno (2019-2025 completi) non deve perdere piu' del 5%
|
|
for y, d in r["yearly"].items():
|
|
if 2019 <= y <= 2025:
|
|
assert d["pnl"] > -0.05, f"anno {y} troppo negativo: {d['pnl']}"
|
|
|
|
|
|
def test_long_only_never_short():
|
|
df = resample_4h(load("ETH", "1h"))
|
|
tp = TrendPortfolio(**CANONICAL) # long_only=True
|
|
assert (tp.target_series(df) >= 0).all()
|
|
|
|
|
|
def test_paper_advance_matches_backtest_slice():
|
|
"""Il loop paper incrementale deve riprodurre l'equity del backtest su una fetta."""
|
|
dfs = _dfs()
|
|
tp = TrendPortfolio(**CANONICAL)
|
|
# backtest portfolio reference (combina i net per timestamp comune)
|
|
series = {}
|
|
for a, df in dfs.items():
|
|
net, ts = tp.net_returns(df)
|
|
series[a] = pd.Series(net, index=pd.to_datetime(ts.values))
|
|
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
|
|
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
|
|
# equity sull'ultimo tratto (skip warmup)
|
|
tail = combo[-500:]
|
|
eq_ref = np.cumprod(1.0 + np.clip(tail, -0.99, None))
|
|
# ricostruzione "alla paper" deve dare lo stesso fattore
|
|
factor = float(eq_ref[-1] / eq_ref[0])
|
|
assert factor > 0
|
|
# sanity: il fattore equivale al prodotto dei (1+combo)
|
|
assert np.isclose(factor, np.prod(1.0 + np.clip(tail, -0.99, None)) / (1.0), rtol=1e-9)
|
|
|
|
|
|
def test_tsmom_blend_range():
|
|
c = np.cumprod(1 + np.random.default_rng(0).normal(0, 0.01, 5000))
|
|
b = tsmom_blend(c, (30, 90, 180))
|
|
assert b.min() >= -1.0 and b.max() <= 1.0
|