docs: timing-sweep pairs/honest (NO deploy) + stato mainnet + vincolo feed v2

- CLAUDE: feed ETH ancora congelato 36h+; vincolo Cerbero v2 (serve SOLO
  5m/15m/1h, 30m/10m -> 400, legacy 404; _SUBHOURLY "30m" speculativo mai
  testato); esito timing-sweep (5m non conviene: regime recente peggiore +
  flat ETH 29%; gate full+OOS necessario ma non sufficiente); stato mainnet
  (token in .env.mainnet verificato is_mainnet=True, conto VUOTO = blocco)
- spec mainnet-microtest: blocco STATO 2026-06-14, .env.mainnet separato +
  servizio dedicato, checklist aggiornata (token done, funding = blocco)
- nuovo diario 2026-06-14-timing-sweep-pairs-honest.md + harness riusabile
  scripts/analysis/timing_sweep_pairs_honest.py
- .gitignore: .env.mainnet (token mainnet mai in git)

Nessuna modifica a codice/config live: PORT06 invariato (19 sleeve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-14 20:19:56 +00:00
parent 47e92a0425
commit c21e1dc635
5 changed files with 475 additions and 6 deletions
@@ -0,0 +1,273 @@
"""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.")