diff --git a/docs/diary/2026-06-12-fade-tf-sweep.md b/docs/diary/2026-06-12-fade-tf-sweep.md new file mode 100644 index 0000000..6ef63b5 --- /dev/null +++ b/docs/diary/2026-06-12-fade-tf-sweep.md @@ -0,0 +1,57 @@ +# 2026-06-12 — FADE TF SWEEP: 1m / 2m / 5m / 10m / 30m (post-swap 15m) + +Richiesta utente: estendere l'analisi timeframe dei fade oltre il 15m appena +deployato (v1.1.30). Script: `scripts/analysis/fade_tf_sweep.py`. +Dati: parquet locale (5m/15m/30m full-history; 10m = resample dal 5m, unit-safe); +1m/2m da Cerbero (120 giorni recenti — la storia 1m locale non esiste: esclusa +dal refresh notturno per costo, 2m/10m non sono intervalli nativi del v2). + +## A. Storia completa (engine canonico, OOS da 2024-10, fee 0.10% RT) + +OOS Sharpe per timeframe (e OOS Sharpe a fee 2x del peggiore): + +| tf | MR01_BTC | MR02_BTC | MR07_BTC | MR01_ETH | MR02_ETH | MR07_ETH | worst f2x | +|----|---------|---------|---------|---------|---------|---------|-----------| +| 5m | 3.66 | 1.90 | 4.12 | 5.31 | 6.54 | 5.52 | **MR02_BTC −1.70** | +| 10m | 2.62 | 2.69 | 3.31 | 5.32 | 6.49 | 5.59 | MR02_BTC 0.32 | +| 15m (live) | 1.94 | 2.30 | 2.37 | 4.94 | 6.40 | 4.44 | MR02_BTC 0.60 | +| 30m | 1.35 | 2.32 | 1.56 | 3.25 | 5.23 | 2.81 | MR02_BTC 1.40 | + +**La frontiera è monotona**: più il tf scende, più Sharpe sale (MR01/MR07)… e più +il margine fee si assottiglia. A fee 2x MR02_BTC muore a 5m e resta fragile a 10m. +MR02 (donchian) fa 3-6x i trade degli altri: è la strategia più esposta al churn. + +## B. Finestra comune recente (2026-02-12 → 06-12, il regime CORRENTE) + +- **MR02 sotto i 15m è un disastro**: 1m −64%, 2m −44%, 5m −22% (fee-death). +- **MR01 a 1m brilla** (ETH +60.6%, Sh 5.7; BTC +33.5%) ma **muore a fee 2x** + (unico sopravvissuto MR01_ETH +16.5%): margine troppo sottile per fidarsi. +- Flat share a 1m: ETH 25.6%, BTC 13.3% → rischio stale-print alto (la lezione + del giorno: pairs-alt/XEX/PAXG). +- Il regime recente è CALMO: anche il 5m vi è fiacco (+4.8/−22.9/+3.7 BTC). + I tf veloci pagano nella volatilità, non nella calma — il loro vantaggio + full-history viene dai regimi mossi (2021-22, 2024). + +## C. Correlazione col 15m live (daily, storia completa) + +5m↔15m media **0.46**, 10m↔15m media **0.53** (range 0.28-0.81). Diversificazione +parziale: un eventuale ADD del 10m avrebbe senso ma è meno pulito del salto +1h→15m (che era a 0.26). + +## Verdetto + +- **1m / 2m: CHIUSI.** Fee-margin nullo a stress, microstruttura flat pesante, + validazione full-history impraticabile. Non deployare mai MR02 sotto i 15m. +- **5m: no-swap.** L'edge c'è ma MR02_BTC muore a fee 2x — viola il criterio di + robustezza fee che tutte le strategie deployate rispettano. +- **10m: in WATCHLIST.** Quasi l'edge del 5m con più margine (f2x 0.32 resta + sotto la soglia di comfort per MR02_BTC; MR01/MR07 reggono bene). Possibile + ADD selettivo (solo MR01/MR07?) da gateare su PORT06 più avanti — NON ora: + il 15m è live da poche ore, un cambio alla volta e si lascia parlare il + ledger reale. +- **15m: confermato** come ginocchio della frontiera margine-fee/rendimento. + +Collaterale tecnico: bug di resample scoperto e fixato nello sweep — pandas 2.x +conserva `datetime64[ms]` da `to_datetime(unit="ms")`, quindi `.view(int64)//10**6` +divide due volte e manda i timestamp nel 1970 (equity piatta silenziosa). Usare +`(index - EPOCH) // pd.Timedelta(milliseconds=1)`. diff --git a/scripts/analysis/fade_tf_sweep.py b/scripts/analysis/fade_tf_sweep.py new file mode 100644 index 0000000..5493e96 --- /dev/null +++ b/scripts/analysis/fade_tf_sweep.py @@ -0,0 +1,160 @@ +"""FADE TF SWEEP — i fade MR01/02/07 su 1m/2m/5m/10m/30m (oltre a 15m live e 1h). + +Ricerca 2026-06-12 (post-swap 15m). Diario: docs/diary/2026-06-12-fade-tf-sweep.md. + +Due banchi di prova: + A. STORIA COMPLETA (parquet locale; 10m = resample dal 5m): engine canonico + build_trades, daily equity su IDX comune, OOS da 2024-10, fee 0.10% e 2x. + B. FINESTRA COMUNE RECENTE (2026-02-12 -> 06-12): 1m/2m (fetch Cerbero, non + esiste storia locale 1m: il refresh la esclude per costo) vs 5m/15m sugli + STESSI 120 giorni — confronto apples-to-apples sul regime corrente. + +Esiti: + - La frontiera Sharpe e' MONOTONA al scendere del tf per MR01/MR07 (full + history OOS: 5m > 10m > 15m > 30m > 1h)... ma il margine fee si assottiglia + insieme: a fee 2x MR02_BTC muore a 5m (-1.70) e resta fragile a 10m (0.32). + - MR02 (donchian, 3-6x i trade degli altri) sotto i 15m muore di fee nel + regime corrente: 1m -64%, 2m -44%, 5m -22% sulla finestra recente. + - 1m/2m: SCARTATI. MR01 a 1m brilla sulla finestra recente (ETH +60%, Sh 5.7) + ma muore a fee 2x, il flat-share 1m e' alto (ETH 25.6%, BTC 13.3% -> rischio + stale-print) e la validazione full-history e' impraticabile (storia 1m non + mantenuta). Il regime recente e' CALMO: anche il 5m vi e' fiacco — i tf + veloci pagano nella volatilita', non nella calma. + - 10m: il miglior candidato OLTRE il 15m (quasi l'edge del 5m con piu' margine + fee; corr daily col 15m live 0.53 media). Eventuale ADD da gateare in + futuro, NON ora: il 15m e' appena andato live (v1.1.30), un cambio alla volta. + - VERDETTO: tenere il 15m (ginocchio della frontiera margine-fee/rendimento); + 10m in watchlist; 1m/2m chiusi; 5m no-swap (fee-fragile su MR02_BTC). + + uv run python scripts/analysis/fade_tf_sweep.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.data.downloader import load_data +from scripts.analysis.risk_management import strats_for, build_trades, INIT, POS +from scripts.analysis.combine_portfolio import IDX, SPLIT, _norm, metrics + +EPOCH = pd.Timestamp(0, tz="UTC") +WINDOW_START = "2026-02-12" # finestra comune del banco B +RECENT_1M = {a: Path(f"/tmp/{a.lower()}_1m_recent.parquet") for a in ("BTC", "ETH")} + + +def resample_ohlcv(df: pd.DataFrame, minutes: int) -> pd.DataFrame: + """Resample OHLCV unit-safe (pandas 2.x conserva datetime64[ms]: niente + aritmetica diretta su .view int64 — il //10**6 doppio manda i ts nel 1970).""" + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + g = df.set_index(ts).resample(f"{minutes}min") + out = pd.DataFrame({"open": g["open"].first(), "high": g["high"].max(), + "low": g["low"].min(), "close": g["close"].last(), + "volume": g["volume"].sum()}).dropna() + out["timestamp"] = (out.index - EPOCH) // pd.Timedelta(milliseconds=1) + return out.reset_index(drop=True) + + +def daily_eq(df: pd.DataFrame, fn, params, fee_rt: float = 0.001) -> tuple[pd.Series, int]: + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + trades = build_trades(fn(df, **params), df, fee_rt=fee_rt, trend_max=3.0) + n = len(df) + eq = np.full(n, INIT) + cap = INIT + for i, j, ret in sorted(trades, key=lambda t: t[1]): + cap = max(cap + cap * POS * ret, 10.0) + eq[j:] = cap + s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill() + return _norm(s), len(trades) + + +def full_history() -> None: + print("=== A. STORIA COMPLETA (OOS da 2024-10, fee 0.10% RT; f2x = OOS Sharpe a fee 2x) ===") + print(f"{'tf':<5} {'sleeve':<10} {'FULL%':>10} {'DD%':>6} {'Sh':>6} | {'OOS%':>8} {'oDD%':>6} {'oSh':>6} | {'f2x_oSh':>7} {'n':>6}") + for tf in ("5m", "10m", "15m", "30m"): + for asset in ("BTC", "ETH"): + df = resample_ohlcv(load_data(asset, "5m"), 10) if tf == "10m" else load_data(asset, tf) + for nm, (fn, params) in strats_for(asset).items(): + eq, n = daily_eq(df, fn, params) + r = eq.pct_change().fillna(0.0) + f, o = metrics(r), metrics(r, lo=SPLIT) + eq2, _ = daily_eq(df, fn, params, fee_rt=0.002) + o2 = metrics(eq2.pct_change().fillna(0.0), lo=SPLIT) + print(f"{tf:<5} {nm + '_' + asset:<10} {f['ret']:>10.0f} {f['dd']:>6.1f} {f['sharpe']:>6.2f}" + f" | {o['ret']:>8.0f} {o['dd']:>6.1f} {o['sharpe']:>6.2f} | {o2['sharpe']:>7.2f} {n:>6}") + print() + + +def trade_stats(df: pd.DataFrame, fn, params, fee_rt: float = 0.001) -> dict: + trades = build_trades(fn(df, **params), df, fee_rt=fee_rt, trend_max=3.0) + cap = peak = INIT + dd = 0.0 + rets = [] + wins = 0 + for i, j, ret in sorted(trades, key=lambda t: t[1]): + cap = max(cap + cap * POS * ret, 10.0) + peak = max(peak, cap) + dd = max(dd, (peak - cap) / peak) + rets.append(ret * POS) + wins += ret > 0 + n = len(trades) + sh = float(np.mean(rets) / np.std(rets) * np.sqrt(n)) if n > 1 and np.std(rets) > 0 else 0.0 + return dict(ret=(cap / INIT - 1) * 100, dd=dd * 100, n=n, + wr=wins / n * 100 if n else 0.0, sh=sh) + + +def recent_window() -> None: + if not all(p.exists() for p in RECENT_1M.values()): + print("\n=== B. saltato: manca il parquet 1m recente (fetch Cerbero, vedi diario) ===") + return + start_ms = int(pd.Timestamp(WINDOW_START, tz="UTC").timestamp() * 1000) + data: dict[tuple[str, str], pd.DataFrame] = {} + for asset in ("BTC", "ETH"): + m1 = pd.read_parquet(RECENT_1M[asset]).sort_values("timestamp").reset_index(drop=True) + data[(asset, "1m")] = m1 + data[(asset, "2m")] = resample_ohlcv(m1, 2) + for tf in ("5m", "15m"): + df = load_data(asset, tf) + data[(asset, tf)] = df[df["timestamp"] >= start_ms].reset_index(drop=True) + + print(f"\n=== B. FINESTRA COMUNE {WINDOW_START} -> oggi (regime corrente) ===") + print("flat share (O=H=L=C):") + for (asset, tf), df in sorted(data.items()): + fl = ((df["open"] == df["high"]) & (df["high"] == df["low"]) & (df["low"] == df["close"])).mean() * 100 + print(f" {asset} {tf:>3}: {fl:5.1f}%") + print(f"\n{'tf':<4} {'sleeve':<10} {'ret%':>8} {'DD%':>6} {'n':>5} {'WR%':>5} {'Sh_tr':>6} | {'fee2x_ret%':>10}") + for tf in ("1m", "2m", "5m", "15m"): + for asset in ("BTC", "ETH"): + for nm, (fn, params) in strats_for(asset).items(): + r1 = trade_stats(data[(asset, tf)], fn, params) + r2 = trade_stats(data[(asset, tf)], fn, params, fee_rt=0.002) + print(f"{tf:<4} {nm + '_' + asset:<10} {r1['ret']:>8.1f} {r1['dd']:>6.1f} {r1['n']:>5}" + f" {r1['wr']:>5.1f} {r1['sh']:>6.2f} | {r2['ret']:>10.1f}") + print() + + +def correlations() -> None: + print("=== C. corr daily (storia completa) vs twin 15m LIVE ===") + c5s, c10s = [], [] + for asset in ("BTC", "ETH"): + d15, d5 = load_data(asset, "15m"), load_data(asset, "5m") + d10 = resample_ohlcv(d5, 10) + for nm, (fn, params) in strats_for(asset).items(): + e15 = daily_eq(d15, fn, params)[0].pct_change() + c5 = daily_eq(d5, fn, params)[0].pct_change().corr(e15) + c10 = daily_eq(d10, fn, params)[0].pct_change().corr(e15) + c5s.append(c5) + c10s.append(c10) + print(f" {nm}_{asset:<4} 5m-15m {c5:.2f} 10m-15m {c10:.2f}") + print(f" media: 5m-15m {np.mean(c5s):.2f} | 10m-15m {np.mean(c10s):.2f}") + + +if __name__ == "__main__": + full_history() + recent_window() + correlations()