"""TIMING SWEEP — famiglie PAIRS & HONEST su 5/10/15/30m (vs live), goal 2026-06-14. Domanda utente: i pairs e le honest beneficiano di timeframe piu' veloci, come hanno fatto le fade (swap 1h->15m, v1.1.30)? VINCOLO DATI (hard): solo BTC/ETH hanno 5m/15m/30m in locale (10m = resample da 5m, qui in data/raw/{a}_10m.parquet temporanei). TUTTI gli alt (ADA/BNB/DOGE/LTC/SOL/XRP) sono SOLO 1h. Conseguenze: - PAIRS: solo ETH/BTC e' sweepabile sub-orario. Gli altri 4 pair (gambe alt: LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL) restano 1h per mancanza di dati alt sub-orari. - HONEST: solo DIP01 (BTC, mean-reversion) ha senso + dati. TR01 (trend EMA 4h su basket alt) e ROT02 (rotazione dual-momentum 1d su universo alt) sono lente (orizzonte multi-giorno/mese) E multi-asset-su-alt -> sub-orario infattibile (dati) e insensato (momentum a 60g su barre 5min). Tutto NETTO (fee 0.10% RT single / 0.20% RT per coppia a 2 gambe), leva 3x, OOS = held-out. Engine CANONICI riusati (pairs_sim_flat, replica dip intrabar == dip_market_gated, gate PORT06 == pairs30m_gate/dip01). Niente re-tuning dei parametri al cambio TF (anti-overfit, come lo swap fade). uv run python scripts/analysis/timing_sweep_pairs_honest.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.pairs_research import pairs_sim_flat from scripts.analysis.report_families import daily_from from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE, IDX, _norm from scripts.portfolios._defs import PORTFOLIOS from src.portfolio.sleeves import all_sleeve_equities from src.portfolio import weighting as W TFS = ["5m", "10m", "15m", "30m", "1h"] BARS_PER_DAY = {"5m": 288, "10m": 144, "15m": 96, "30m": 48, "1h": 24} LEV, POS = 3.0, 0.15 EPOCH = pd.Timestamp(0, tz="UTC") def _ensure_10m(): """10m non e' nativo (download_all fa 5m/15m/30m/1h): lo resampla da 5m se manca. Aggregazione OHLCV causale (first/max/min/last/sum). File gitignored, rigenerabile.""" for a in ("btc", "eth"): dst = PROJECT_ROOT / f"data/raw/{a}_10m.parquet" if dst.exists(): continue df = load_data(a.upper(), "5m").sort_values("timestamp").reset_index(drop=True) ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) g = df.set_index(ts).resample("10min") 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) out.reset_index(drop=True).to_parquet(dst, index=False) print(f" [bootstrap] generato {dst.name} ({len(out)} righe da 5m)") # config UNIVERSALE pairs (1h, CLAUDE.md) — NON ri-tunata al cambio TF (anti-overfit) PAIR_CFG = dict(n=50, z_in=2.0, z_exit=0.75, max_bars=72) # config DIP01 canonica (dip_market_gated default, market_n=0 = live) DIP_CFG = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24) # ============================ helper dati ============================ def flat_share(asset, tf): df = load_data(asset, tf) o, h, l, c = df["open"], df["high"], df["low"], df["close"] return ((o == h) & (h == l) & (l == c)).mean() * 100 # ============================ DIP01 engine TF-aware ============================ def _atr(df, n=14): h, l, c = df["high"].values, df["low"].values, df["close"].values pc = np.roll(c, 1); pc[0] = c[0] tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc))) return pd.Series(tr).rolling(n).mean().values def dip_sim(asset, tf, n=50, z_in=2.5, sl_atr=2.5, max_bars=24, fee_rt=0.001, oos_frac=0.0): """Replica TF-aware di honest_improve2.dip_market_gated(market_n=0): dip-buy z-score, TP=SMA intrabar, SL=close-sl_atr*ATR intrabar (SL prioritario), max_bars. Engine canonico.""" df = load_data(asset, tf) h, l, c = df["high"].values, df["low"].values, df["close"].values N = len(c); ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) ma = pd.Series(c).rolling(n).mean().values sd = pd.Series(c).rolling(n).std().values a = _atr(df, 14) z = (c - ma) / np.where(sd == 0, np.nan, sd) fee = fee_rt * LEV cap = peak = 1000.0; dd = 0.0; last_exit = -1 eq_ts, eq_v = [], []; rets = []; trades = wins = 0; yearly = {} split = int(N * (1 - oos_frac)) if oos_frac else 0 for i in range(n + 14, N): if i < split or np.isnan(z[i]) or np.isnan(a[i]): continue if not (z[i] <= -z_in and z[i - 1] > -z_in): continue if i <= last_exit or i + 1 >= N: continue entry = c[i]; tp, sl, mb = ma[i], c[i] - sl_atr * a[i], max_bars exit_p = c[min(i + mb, N - 1)]; j = min(i + mb, N - 1) for k in range(1, mb + 1): j = i + k if j >= N: j = N - 1; exit_p = c[j]; break if l[j] <= sl: exit_p = sl; break if h[j] >= tp: exit_p = tp; break if k == mb: exit_p = c[j] ret = (exit_p - entry) / entry * LEV - fee cap = max(cap + cap * POS * ret, 10.0) peak = max(peak, cap); dd = max(dd, (peak - cap) / peak) last_exit = j; trades += 1; wins += ret > 0; rets.append(ret * POS) yearly[ts.iloc[i].year] = yearly.get(ts.iloc[i].year, 0.0) + ret * 100 eq_ts.append(ts.iloc[j]); eq_v.append(cap) yrs = (ts.iloc[-1] - ts.iloc[max(split, 0)]).days / 365.25 or 1 sh = float(np.mean(rets) / np.std(rets) * np.sqrt(trades / yrs)) if len(rets) > 1 and np.std(rets) > 0 else 0.0 return dict(trades=trades, win=wins / trades * 100 if trades else 0, ret=(cap / 1000 - 1) * 100, dd=dd * 100, sharpe=sh, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v) # ============================ gate PORT06 ============================ def port_metrics(members, ids, clusters, caps): dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids}) w = W.weight_vector("cap", ids, dr, caps=caps, clusters=clusters) drp = port_returns({i: members[i] for i in ids}, w) return metrics(drp), metrics(drp, lo=SPLIT), w def daily_norm(eq_ts, eq_v): return _norm(daily_from(eq_ts, eq_v)) # ============================ PART 1: dati ============================ def part1_data(): print("=" * 96) print(" PART 1 — REALTA' DATI: flat-share (O=H=L=C, = print stale, rischio fill) per TF") print("=" * 96) print(f" {'asset':<6}" + "".join(f"{tf:>8}" for tf in TFS)) for a in ("BTC", "ETH"): print(f" {a:<6}" + "".join(f"{flat_share(a, tf):>7.1f}%" for tf in TFS)) print("\n ALT (ADA/BNB/DOGE/LTC/SOL/XRP): SOLO 1h disponibile -> NON sweepabili sub-orario.") print(" => pairs con gamba alt (4/5) e honest multi-asset (TR01/ROT02) bloccati a 1h dai dati.") # ============================ PART 2: pairs ETH/BTC standalone ============================ def part2_pairs_standalone(): print("\n" + "=" * 96) print(" PART 2 — PAIRS ETH/BTC standalone, config UNIVERSALE n=50 z_in=2.0 z_exit=0.75 mb=72") print(f" (flat_skip=True, live-realizable; OOS held-out; fee 0.20% RT/coppia; f2x = OOS Sh a fee 2x)") print("=" * 96) print(f" {'tf':<5}{'trd':>7}{'FULL%':>9}{'DD%':>7}{'Sh':>7} | {'OOS%':>9}{'oDD%':>7}{'oSh':>7} | {'f2x_oSh':>8}{'mb_h':>6}") eqs = {} for tf in TFS: f = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True) o = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, split_frac=1 - 0.30) o2 = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, split_frac=1 - 0.30, fee_rt=0.002) eqs[tf] = daily_norm(f["eq_ts"], f["eq_v"]) mb_h = PAIR_CFG["max_bars"] / BARS_PER_DAY[tf] * 24 print(f" {tf:<5}{f['trades']:>7}{f['ret']:>9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" f" | {o['ret']:>9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f} | {o2['sharpe']:>8.2f}{mb_h:>6.1f}") # correlazioni daily fra TF print("\n CORR rendimenti daily fra TF (alta = ridondante):") print(f" {'':<6}" + "".join(f"{tf:>7}" for tf in TFS)) for t1 in TFS: row = [] for t2 in TFS: c = eqs[t1].pct_change().fillna(0).corr(eqs[t2].pct_change().fillna(0)) row.append(f"{c:>7.2f}") print(f" {t1:<6}" + "".join(row)) return eqs # ============================ PART 3: pairs gate PORT06 ============================ def part3_pairs_gate(eqs): print("\n" + "=" * 96) print(" PART 3 — GATE PORT06: aggiungere ETH/BTC 5m e/o 10m al BLEND live (1h+15m), mezza size") print(f" (baseline = sleeve canonici live; OOS da {OOS_DATE})") print("=" * 96) p = PORTFOLIOS["PORT06"] base = dict(all_sleeve_equities()) # include PR_ETHBTC (1h) + PR_ETHBTC_15M ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps f0, o0, _ = port_metrics(base, ids0, cl0, caps) print(f" {'config':<26}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}") print(f" {'ATTUALE (1h+15m)':<26}{f0['sharpe']:>9.2f}{f0['dd']:>10.2f}{o0['sharpe']:>9.2f}{o0['dd']:>9.2f}") # half-size pairs equity (pos 0.075 come 15m live): ricalcolo eq a pos dimezzato for tf in ("10m", "5m"): fr = pairs_sim_flat("ETH", "BTC", tf=tf, **PAIR_CFG, flat_skip=True, pos=0.075) cand = daily_norm(fr["eq_ts"], fr["eq_v"]) mem = dict(base); sid = f"PR_ETHBTC_{tf.upper()}" mem[sid] = cand ids = ids0 + [sid]; cl = dict(cl0); cl[sid] = "ETH-rev" f1, o1, w1 = port_metrics(mem, ids, cl, caps) ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9 and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9) verdict = "MIGLIORA (promosso)" if ok else "non domina (vedi numeri)" print(f" {'+' + tf + ' (half size)':<26}{f1['sharpe']:>9.2f}{f1['dd']:>10.2f}{o1['sharpe']:>9.2f}{o1['dd']:>9.2f} {verdict}") # ============================ PART 4: DIP01 ============================ def part4_dip(): print("\n" + "=" * 96) print(" PART 4 — HONEST/DIP01 (BTC) standalone, config canonica n=50 z_in=2.5 sl_atr=2.5 mb=24") print(" (engine == dip_market_gated market_n=0; OOS held-out; fee 0.10% RT; f2x = OOS Sh a fee 2x)") print("=" * 96) print(f" {'tf':<5}{'asset':<5}{'trd':>7}{'WR%':>6}{'FULL%':>9}{'DD%':>7}{'Sh':>7} | {'OOS%':>9}{'oDD%':>7}{'oSh':>7} | {'f2x_oSh':>8}{'mb_h':>6}") eqs = {} for asset in ("BTC", "ETH"): for tf in TFS: f = dip_sim(asset, tf, **DIP_CFG) o = dip_sim(asset, tf, **DIP_CFG, oos_frac=0.30) o2 = dip_sim(asset, tf, **DIP_CFG, oos_frac=0.30, fee_rt=0.002) if asset == "BTC": eqs[tf] = daily_norm(f["eq_ts"], f["eq_v"]) if f["eq_v"] else None mb_h = DIP_CFG["max_bars"] / BARS_PER_DAY[tf] * 24 print(f" {tf:<5}{asset:<5}{f['trades']:>7}{f['win']:>6.1f}{f['ret']:>9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" f" | {o['ret']:>9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f} | {o2['sharpe']:>8.2f}{mb_h:>6.1f}") print() # corr daily BTC fra TF print(" CORR rendimenti daily DIP01 BTC fra TF:") print(f" {'':<6}" + "".join(f"{tf:>7}" for tf in TFS)) for t1 in TFS: row = [] for t2 in TFS: if eqs.get(t1) is None or eqs.get(t2) is None: row.append(f"{'-':>7}"); continue c = eqs[t1].pct_change().fillna(0).corr(eqs[t2].pct_change().fillna(0)) row.append(f"{c:>7.2f}") print(f" {t1:<6}" + "".join(row)) return eqs # ============================ PART 5: DIP01 gate PORT06 ============================ def part5_dip_gate(eqs): print("\n" + "=" * 96) print(" PART 5 — GATE PORT06: SWAP DIP01_BTC 1h -> TF piu' veloce (sostituisce lo sleeve)") print("=" * 96) p = PORTFOLIOS["PORT06"] base = dict(all_sleeve_equities()) ids0 = list(p.sleeve_ids); cl0 = p.clusters; caps = p.caps f0, o0, _ = port_metrics(base, ids0, cl0, caps) print(f" {'config':<26}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}") print(f" {'ATTUALE (DIP01 1h)':<26}{f0['sharpe']:>9.2f}{f0['dd']:>10.2f}{o0['sharpe']:>9.2f}{o0['dd']:>9.2f}") for tf in ("30m", "15m", "10m", "5m"): if eqs.get(tf) is None: continue mem = dict(base); mem["DIP01_BTC"] = eqs[tf] f1, o1, _ = port_metrics(mem, ids0, cl0, caps) ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9 and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9) verdict = "MIGLIORA" if ok else "non domina" print(f" {'DIP01 ' + tf:<26}{f1['sharpe']:>9.2f}{f1['dd']:>10.2f}{o1['sharpe']:>9.2f}{o1['dd']:>9.2f} {verdict}") if __name__ == "__main__": _ensure_10m() part1_data() pe = part2_pairs_standalone() part3_pairs_gate(pe) de = part4_dip() part5_dip_gate(de) print("\n NB TR01/ROT02: nessuno sweep — dati alt solo 1h + orizzonte multi-giorno/mese") print(" (trend EMA20/100 4h, rotazione momentum 60g 1d) rendono il sub-orario infattibile e insensato.")