"""r0702_crt_context.py — CRT CON CONTESTO (2026-07-02). FILONE: la scuola "Candle Range Theory" dice che lo sweep-and-reclaim vale SOLO su zone importanti (liquidita' sopra massimi/minimi rilevanti, FVG, sessione giusta). Qui testiamo se i FILTRI DI CONTESTO trasformano un fade (gia' morto in versione generica sul feed certificato) in un edge. ONESTA' PRIMA DI TUTTO. SETUP BASE FISSO (identico per tutte le celle, deciso A PRIORI prima di guardare i numeri): * TF in {1h, 4h} (4h = resample leak-free da 1h via altlib.get). * C2 = barra che SUPERA un livello di riferimento e CHIUDE dal lato opposto: SHORT: high[i] > lvl_hi AND close[i] < lvl_hi (sweep del massimo + reclaim) LONG : low[i] < lvl_lo AND close[i] > lvl_lo (sweep del minimo + reclaim) (barra che sweppa ENTRAMBI i livelli e chiude in mezzo = ambigua -> scartata, dichiarato) * Entry a close[i] (decisione con dati <= close[i], eseguibile). * Stop DIETRO l'estremo di C2: estremo +/- 0.10*ATR14 (causale). * Target = R FISSO 1.5:1 (scelto a priori; NON centro-range: il centro di un Donchian in trend e' asimmetrico/ambiguo, R fisso e' uniforme su tutti i level-type). * max_hold 20 barre; fill conservativi (SL prioritario se TP e SL nella stessa barra, identico all'harness src/backtest/harness.backtest_signals); fee 0.10% RT. LIVELLI (tutti causali, shift(1) su aggregati di periodi COMPLETI precedenti): prevday = high/low del giorno UTC precedente (barre open-labeled, groupby giorno -> shift) don20/55 = max/min delle N barre STRETTAMENTE precedenti (al.donchian, shift(1) built-in) prevweek = high/low della settimana ISO precedente (lunedi' 00 UTC) ⚠️ OVERLAP DICHIARATO col lead PREVDAY-BREAKOUT in forward-monitor (src/strategies/prevday_breakout.py + scripts/live/paper_prevday.py): STESSI livelli prior-day, condizionamento OPPOSTO — il lead SEGUE il break decisivo (close > lvl + 0.30*range), qui si FADEA il reclaim (close torna dentro). Se entrambi avessero edge sugli stessi livelli, uno dei due e' rumore -> confronto esplicito fade-vs-follow (corr daily + chi vince dove) in fondo. FILTRI DI CONTESTO (la parte "discrezionale" della CRT, meccanizzata): EQ = equal highs/lows: il livello e' stato toccato >=2 volte entro 0.10*ATR14 nelle ultime N barre (N = lookback del livello stesso; prevday=2 giorni, prevweek=7 giorni). FVG = esiste un fair-value-gap a 3 candele NON ancora riempito nelle ultime 20 barre, nella direzione del trade (short: FVG bullish sotto il prezzo non riempito = magnete giu'; long: FVG bearish sopra non riempito). Meccanizzazione SEMPLICE di un concetto fuzzy discrezionale — limiti dichiarati: k=20 fisso, zona "non riempita" = mai traversata interamente, nessuna nozione di "displacement" o "premium/discount". SES = sessione dello sweep (ora UTC di apertura barra): Asia 00-08 / Europa 08-14 / US 14-22. Solo a 1h (a 4h la label di sessione e' troppo grossolana). ⚠️ OGNI cella sessione passa un anchor-shift +/-2/4h (analogo di al.day_boundary_robust a livello trade) prima di essere creduta. GATES: selezione SOLO in-sample pre-2025 (HOLDOUT altlib = 2025-01-01); hold-out riportato mai usato per scegliere; al.deflated_sharpe su TUTTI i 22 trial; fee sweep 0.00-0.20% RT; se il best-IS regge (Sharpe >= 0.5) -> al.marginal_vs_tp01. Causalita': livelli ricalcolati su prefisso troncato e confrontati (check esplicito in fondo). Niente .view("int64"), niente ffill mixed-TF. Uso: uv run python scripts/research/r0702_crt_context.py """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al # noqa: E402 import numpy as np # noqa: E402 import pandas as pd # noqa: E402 ROOT = Path("/opt/docker/PythagorasGoal") if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from src.backtest.harness import backtest_signals # noqa: E402 from src.strategies.prevday_breakout import target as prevday_follow_target # noqa: E402 HOLDOUT = al.HOLDOUT FEE_RT = 0.001 # 0.10% round-trip (Deribit taker) MAX_HOLD = 20 # barre R_MULT = 1.5 # target R fisso 1.5:1 (a priori) SL_ATR_BUF = 0.10 # stop = estremo C2 +/- 0.10*ATR14 (a priori) EQ_TOL_ATR = 0.10 # tolleranza equal highs/lows EQ_MIN_TOUCH = 2 FVG_K = 20 # lookback barre per FVG non riempito ASSETS = ("BTC", "ETH") SESSIONS = {"asia": (0, 8), "eu": (8, 14), "us": (14, 22)} LEVELS = ("prevday", "don20", "don55", "prevweek") # =========================================================================== # LIVELLI (causali) # =========================================================================== def prior_day_levels(df: pd.DataFrame, shift_h: int = 0): """High/low del giorno UTC PRECEDENTE (shift(1) sul groupby giorno -> strettamente prima di oggi). shift_h sposta il confine del giorno (per l'anchor-shift test).""" dt = pd.to_datetime(df["datetime"], utc=True) + pd.Timedelta(hours=shift_h) day = dt.dt.floor("1D") g = pd.DataFrame({"day": day.values, "high": df["high"].values.astype(float), "low": df["low"].values.astype(float)}) per = g.groupby("day").agg(dh=("high", "max"), dl=("low", "min")) m = pd.DataFrame({"dh": per["dh"].shift(1), "dl": per["dl"].shift(1)}).reindex(g["day"].values) return m["dh"].values, m["dl"].values def prior_week_levels(df: pd.DataFrame): """High/low della settimana ISO PRECEDENTE (lunedi' 00 UTC, shift(1)).""" dt = pd.to_datetime(df["datetime"], utc=True) day = dt.dt.floor("1D") week = (day - pd.to_timedelta(dt.dt.dayofweek, unit="D")) g = pd.DataFrame({"wk": week.values, "high": df["high"].values.astype(float), "low": df["low"].values.astype(float)}) per = g.groupby("wk").agg(wh=("high", "max"), wl=("low", "min")) m = pd.DataFrame({"wh": per["wh"].shift(1), "wl": per["wl"].shift(1)}).reindex(g["wk"].values) return m["wh"].values, m["wl"].values def get_levels(df: pd.DataFrame, level: str, shift_h: int = 0): if level == "prevday": return prior_day_levels(df, shift_h) if level == "prevweek": return prior_week_levels(df) if level == "don20": return al.donchian(df, 20) if level == "don55": return al.donchian(df, 55) raise ValueError(level) def level_lookback_bars(level: str, bpd: int) -> int: """Lookback per il conteggio equal-touch = finestra del livello stesso.""" return {"prevday": 2 * bpd, "prevweek": 7 * bpd, "don20": 20, "don55": 55}[level] # =========================================================================== # EVENTI (sweep-and-reclaim) + outcome trade-level (overlap PERMESSO -> paired analysis) # =========================================================================== def _unfilled_fvg(h: np.ndarray, l: np.ndarray, i: int, d: int, price: float) -> bool: """SHORT (d=-1): esiste FVG BULLISH (low[j] > high[j-2]) nelle ultime FVG_K barre con zona (high[j-2], low[j]) interamente SOTTO il prezzo e mai riempita (nessuna barra dopo j e' scesa fino al bordo inferiore). LONG (d=+1): simmetrico con FVG bearish sopra.""" j0 = max(2, i - FVG_K) for j in range(i - 1, j0 - 1, -1): if d == -1 and l[j] > h[j - 2]: zone_lo, zone_hi = h[j - 2], l[j] if zone_hi < price and np.min(l[j + 1:i + 1]) > zone_lo: return True if d == 1 and h[j] < l[j - 2]: zone_lo, zone_hi = h[j], l[j - 2] if zone_lo > price and np.max(h[j + 1:i + 1]) < zone_hi: return True return False def build_events(df: pd.DataFrame, level: str, shift_h: int = 0, with_context: bool = True) -> pd.DataFrame: """Tabella eventi sweep-and-reclaim con outcome trade-level (entry close[i], exit da i+1, SL prioritario, fee 0.10% RT) + feature di contesto (eq/fvg/session). Overlap permesso: ogni evento valutato indipendentemente -> confronto PAIRED filtro-vs-tutti pulito.""" h = df["high"].values.astype(float) l = df["low"].values.astype(float) c = df["close"].values.astype(float) n = len(c) lvl_hi, lvl_lo = get_levels(df, level, shift_h) a14 = al.atr(df, 14) dt = pd.to_datetime(df["datetime"], utc=True) hours = dt.dt.hour.values bpd = al.bars_per_day(df) lb = level_lookback_bars(level, bpd) sw_hi = np.isfinite(lvl_hi) & (h > lvl_hi) & (c < lvl_hi) sw_lo = np.isfinite(lvl_lo) & (l < lvl_lo) & (c > lvl_lo) both = sw_hi & sw_lo sw_hi &= ~both sw_lo &= ~both rows = [] for i in np.where(sw_hi | sw_lo)[0]: if i >= n - 1 or i < 60: continue d = -1 if sw_hi[i] else 1 entry = c[i] atr_i = a14[i] if not np.isfinite(atr_i) or atr_i <= 0: continue if d == -1: sl = h[i] + SL_ATR_BUF * atr_i risk = sl - entry tp = entry - R_MULT * risk else: sl = l[i] - SL_ATR_BUF * atr_i risk = entry - sl tp = entry + R_MULT * risk if risk <= 0 or tp <= 0: continue jend = min(i + MAX_HOLD, n - 1) exit_price = c[jend] for j in range(i + 1, jend + 1): if d == 1: if l[j] <= sl: exit_price = sl break if h[j] >= tp: exit_price = tp break else: if h[j] >= sl: exit_price = sl break if l[j] <= tp: exit_price = tp break exit_price = c[j] gross = d * (exit_price - entry) / entry L = lvl_hi[i] if d == -1 else lvl_lo[i] row = dict(i=int(i), dir=int(d), entry=entry, sl=sl, tp=tp, level_px=float(L), atr=float(atr_i), gross=gross, net=gross - FEE_RT) if with_context: j0 = max(0, i - lb) tol = EQ_TOL_ATR * atr_i touches = int(np.sum(np.abs((h if d == -1 else l)[j0:i] - L) <= tol)) row["eq"] = touches >= EQ_MIN_TOUCH row["fvg"] = _unfilled_fvg(h, l, int(i), int(d), float(entry)) hr = int(hours[i]) row["ses"] = next((s for s, (a, b) in SESSIONS.items() if a <= hr < b), "none") rows.append(row) ev = pd.DataFrame(rows) if len(ev): ev["dt"] = dt.values[ev["i"].values] ev["hold"] = pd.to_datetime(ev["dt"], utc=True) >= HOLDOUT ev["year"] = pd.to_datetime(ev["dt"], utc=True).dt.year return ev # =========================================================================== # STRATEGIA (non-overlap, harness ufficiale) — metriche daily-step # =========================================================================== def entries_from(df: pd.DataFrame, sub: pd.DataFrame) -> list: ent: list = [None] * len(df) for row in sub.itertuples(): ent[row.i] = dict(dir=int(row.dir), tp=float(row.tp), sl=float(row.sl), max_bars=MAX_HOLD) return ent def strat_eval(df: pd.DataFrame, entries: list, fee_rt: float = FEE_RT) -> dict: m = backtest_signals(df, entries, fee_rt=fee_rt, leverage=1.0) idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) eq = pd.Series(m.equity, index=idx) d = eq.resample("1D").last().dropna().pct_change().dropna() di, dh = d[d.index < HOLDOUT], d[d.index >= HOLDOUT] return dict(n_trades=int(m.n_trades), wr=round(m.win_rate, 1), dd=round(m.max_dd, 4), sh_full=round(al._sh(d), 3), sh_is=round(al._sh(di), 3), sh_hold=round(al._sh(dh), 3), daily=d) def apply_filter(ev: pd.DataFrame, filt: str | None) -> pd.DataFrame: if not len(ev) or filt is None: return ev if filt == "eq": return ev[ev["eq"]] if filt == "fvg": return ev[ev["fvg"]] if filt.startswith("ses_"): return ev[ev["ses"] == filt[4:]] raise ValueError(filt) def eval_trial(events: dict, tf: str, level: str, filt: str | None) -> dict: per_asset, dailies = {}, {} for a in ASSETS: df = al.get(a, tf) ev = events[(a, tf, level)] sub = apply_filter(ev, filt) r = strat_eval(df, entries_from(df, sub)) yrs = (pd.to_datetime(df["datetime"].iloc[-1], utc=True) - pd.to_datetime(df["datetime"].iloc[0], utc=True)).days / 365.25 per_asset[a] = dict(r, n_ev=len(sub), ev_per_yr=round(len(sub) / yrs, 1), exp_is=_exp(sub, False), exp_hold=_exp(sub, True)) dailies[a] = r["daily"] J = pd.concat(dailies, axis=1, join="inner").fillna(0.0) comb = J.mean(axis=1) ci, ch = comb[comb.index < HOLDOUT], comb[comb.index >= HOLDOUT] return dict(tf=tf, level=level, filt=filt or "-", per_asset=per_asset, comb_daily=comb, sh_is=round(al._sh(ci), 3), sh_hold=round(al._sh(ch), 3), sh_full=round(al._sh(comb), 3), min_sh_is=round(min(per_asset[a]["sh_is"] for a in ASSETS), 3), min_sh_hold=round(min(per_asset[a]["sh_hold"] for a in ASSETS), 3)) def _exp(sub: pd.DataFrame, hold: bool): """Expectancy netta per trade (%) sullo slice IS/HOLD.""" if not len(sub): return None s = sub[sub["hold"] == hold]["net"] return round(float(s.mean()) * 100, 3) if len(s) else None # =========================================================================== # MAIN # =========================================================================== def main(): print("=" * 100) print("r0702 CRT CON CONTESTO — sweep-and-reclaim su livelli + filtri (EQ/FVG/sessione)") print(f"setup fisso: entry close C2, SL estremo+/-{SL_ATR_BUF}*ATR14, TP R={R_MULT}:1, " f"max_hold {MAX_HOLD} barre, fee {FEE_RT*100:.2f}% RT, SL prioritario same-bar") print("=" * 100) # ---------- 0. CAUSALITY CHECK sui livelli (prefisso troncato) ---------- print("\n[0] CAUSALITY CHECK livelli (ricalcolo su prefisso troncato, tail 200 barre)") for level in LEVELS: worst = 0.0 for a in ASSETS: df = al.get(a, "1h") cut = int(len(df) * 0.9) hi_f, lo_f = get_levels(df, level) sub = df.iloc[:cut].reset_index(drop=True) hi_s, lo_s = get_levels(sub, level) for full, part in ((hi_f, hi_s), (lo_f, lo_s)): x, y = np.nan_to_num(full[cut - 200:cut]), np.nan_to_num(part[cut - 200:cut]) worst = max(worst, float(np.max(np.abs(x - y)))) print(f" {level:<9s} max_tail_diff={worst:.10f} {'OK' if worst < 1e-9 else 'FAIL'}") # ---------- 1. EVENTI (cache) ---------- events = {} for a in ASSETS: for tf in ("1h", "4h"): df = al.get(a, tf) for level in LEVELS: events[(a, tf, level)] = build_events(df, level) # ---------- 2. TRIALS (22, definiti a priori) ---------- trials = [] for tf in ("1h", "4h"): for level in LEVELS: trials.append((tf, level, None)) for tf in ("1h", "4h"): for level in ("don20", "prevday"): trials.append((tf, level, "eq")) trials.append((tf, level, "fvg")) for level in ("don20", "prevday"): for ses in SESSIONS: trials.append(("1h", level, f"ses_{ses}")) assert len(trials) == 22 results = {} for tf, level, filt in trials: results[(tf, level, filt or "-")] = eval_trial(events, tf, level, filt) # ---------- 3. BASELINE INCONDIZIONATA (don20, nessun filtro) ---------- print("\n[1] BASELINE INCONDIZIONATA — sweep-and-reclaim Donchian20, nessun contesto") for tf in ("1h", "4h"): r = results[(tf, "don20", "-")] print(f" TF {tf}: comb Sharpe IS={r['sh_is']:+.2f} HOLD={r['sh_hold']:+.2f} " f"FULL={r['sh_full']:+.2f}") for a in ASSETS: p = r["per_asset"][a] print(f" {a}: ev/yr={p['ev_per_yr']:>6.1f} trades(no-overlap)={p['n_trades']:>5d} " f"wr={p['wr']:>4.1f}% expIS={p['exp_is']}% expHOLD={p['exp_hold']}% " f"Sh IS={p['sh_is']:+.2f} HOLD={p['sh_hold']:+.2f} DD={p['dd']*100:.1f}%") # ---------- 4. TUTTE LE CELLE (tabella) ---------- print("\n[2] TUTTE LE 22 CELLE (comb 50/50, daily-step Sharpe; exp = %/trade netto)") print(f" {'tf':<3s} {'level':<9s} {'filt':<9s} {'ShIS':>6s} {'ShHOLD':>7s} {'ShFULL':>7s} " f"{'minShIS':>8s} {'minShHOLD':>9s} {'BTCexpIS':>9s} {'ETHexpIS':>9s} " f"{'BTCexpH':>8s} {'ETHexpH':>8s} {'nBTC':>5s} {'nETH':>5s}") for (tf, level, filt), r in sorted(results.items(), key=lambda kv: -kv[1]["sh_is"]): pb, pe = r["per_asset"]["BTC"], r["per_asset"]["ETH"] print(f" {tf:<3s} {level:<9s} {filt:<9s} {r['sh_is']:>+6.2f} {r['sh_hold']:>+7.2f} " f"{r['sh_full']:>+7.2f} {r['min_sh_is']:>+8.2f} {r['min_sh_hold']:>+9.2f} " f"{str(pb['exp_is']):>9s} {str(pe['exp_is']):>9s} " f"{str(pb['exp_hold']):>8s} {str(pe['exp_hold']):>8s} " f"{pb['n_ev']:>5d} {pe['n_ev']:>5d}") # ---------- 5. UPLIFT PAIRED dei filtri (stessi eventi, sottoinsieme vs tutti) ---------- print("\n[3] UPLIFT PAIRED per filtro (expectancy %/trade: filtrato - tutti; stessi eventi)") print(f" {'base':<16s} {'filtro':<9s} {'asset':<4s} {'slice':<5s} {'n_all':>6s} {'n_f':>5s} " f"{'exp_all':>8s} {'exp_f':>8s} {'uplift':>8s}") filt_names = ["eq", "fvg"] + [f"ses_{s}" for s in SESSIONS] uplift_summary = {} for tf in ("1h", "4h"): for level in ("don20", "prevday"): for filt in filt_names: if filt.startswith("ses_") and tf != "1h": continue key = (tf, level, filt) for a in ASSETS: ev = events[(a, tf, level)] sub = apply_filter(ev, filt) for hold, lab in ((False, "IS"), (True, "HOLD")): ea, ef = _exp(ev, hold), _exp(sub, hold) na = int((ev["hold"] == hold).sum()) if len(ev) else 0 nf = int((sub["hold"] == hold).sum()) if len(sub) else 0 up = round(ef - ea, 3) if (ea is not None and ef is not None) else None uplift_summary.setdefault(key, []).append((a, lab, up)) print(f" {tf+'/'+level:<16s} {filt:<9s} {a:<4s} {lab:<5s} {na:>6d} " f"{nf:>5d} {str(ea):>8s} {str(ef):>8s} {str(up):>8s}") print("\n Consistenza uplift per filtro (positivo su TUTTE le 4 slice asset x IS/HOLD?):") for key, ups in uplift_summary.items(): vals = [u for (_, _, u) in ups if u is not None] n_pos = sum(1 for u in vals if u > 0) print(f" {key[0]}/{key[1]}+{key[2]:<9s}: {n_pos}/{len(vals)} slice positive " f"{'<-- consistente' if vals and n_pos == len(vals) else ''}") # ---------- 6. SELEZIONE IN-SAMPLE + DSR ---------- all_sr = [r["sh_full"] for r in results.values()] chosen_key = max(results, key=lambda k: results[k]["sh_is"]) ch = results[chosen_key] dsr, sr0 = al.deflated_sharpe(ch["sh_full"], all_sr, ch["comb_daily"]) print(f"\n[4] SELEZIONE IN-SAMPLE-ONLY (pre-2025) su {len(trials)} trial") print(f" best-IS: {chosen_key} ShIS={ch['sh_is']:+.2f} ShHOLD={ch['sh_hold']:+.2f} " f"ShFULL={ch['sh_full']:+.2f}") print(f" deflated Sharpe (n_trials={len(all_sr)}): DSR={dsr:.3f} " f"(PASS>=0.95) expected-null-max Sharpe={sr0:.2f}") # fee sweep sul best-IS e sulla baseline don20/1h print("\n[5] FEE SWEEP (Sharpe FULL comb per fee RT)") for key in {chosen_key, ("1h", "don20", "-")}: tf, level, filt = key row = [] for fee in (0.0, 0.0005, 0.001, 0.0015, 0.002): dailies = {} for a in ASSETS: df = al.get(a, tf) sub = apply_filter(events[(a, tf, level)], None if filt == "-" else filt) # ricalcola net eventi con fee diversa e' lineare; per la strategia rifacciamo il bt dailies[a] = strat_eval(df, entries_from(df, sub), fee_rt=fee)["daily"] J = pd.concat(dailies, axis=1, join="inner").fillna(0.0) row.append(f"{fee*100:.2f}%RT:{al._sh(J.mean(axis=1)):+.2f}") print(f" {key}: " + " ".join(row)) # ---------- 7. ANCHOR-SHIFT sulle celle sessione (+/-2/4h) ---------- print("\n[6] ANCHOR-SHIFT celle sessione (label ora spostata; uplift expectancy IS per shift)") for level in ("don20", "prevday"): for ses in SESSIONS: per_shift = {} for sh in (-4, -2, 0, 2, 4): ups = [] for a in ASSETS: ev = events[(a, "1h", level)] if not len(ev): continue hrs = (pd.to_datetime(ev["dt"], utc=True).dt.hour + sh) % 24 a_, b_ = SESSIONS[ses] sub = ev[(hrs >= a_) & (hrs < b_)] ea, ef = _exp(ev, False), _exp(sub, False) if ea is not None and ef is not None: ups.append(ef - ea) per_shift[sh] = round(float(np.mean(ups)), 3) if ups else None vals = [v for v in per_shift.values() if v is not None] flip = vals and (max(vals) > 0 > min(vals)) and (max(vals) - min(vals)) > 0.05 verd = "ARTIFACT-RISK(flip)" if flip else \ ("stabile-pos" if vals and min(vals) > 0 else "stabile-neg/nullo" if vals and max(vals) <= 0 else "misto-debole") print(f" {level}+{ses:<5s}: " + " ".join(f"{k:+d}h:{v}" for k, v in per_shift.items()) + f" -> {verd}") # ---------- 8. DAY-BOUNDARY SHIFT sul fade prevday base (1h) ---------- print("\n[7] DAY-BOUNDARY SHIFT su fade prevday base 1h (livelli ricostruiti col giorno spostato)") for sh in (0, 2, 4, 8, 12): dailies = {} for a in ASSETS: df = al.get(a, "1h") ev = build_events(df, "prevday", shift_h=sh, with_context=False) dailies[a] = strat_eval(df, entries_from(df, ev))["daily"] J = pd.concat(dailies, axis=1, join="inner").fillna(0.0) comb = J.mean(axis=1) ci = comb[comb.index < HOLDOUT] print(f" shift +{sh:>2d}h: Sh IS={al._sh(ci):+.2f} FULL={al._sh(comb):+.2f}") # ---------- 9. FADE vs FOLLOW sui livelli prior-day (lead esistente) ---------- print("\n[8] FADE (questo filone, prevday base 1h) vs FOLLOW (lead prevday_breakout congelato)") fol = {} for a in ASSETS: df = al.get(a, "1h") evw = al.eval_weights(df, prevday_follow_target(df)) fol[a] = pd.Series(evw["net"], index=evw["idx"]) Jf = pd.concat(fol, axis=1, join="inner").fillna(0.0) follow_d = al._to_daily(0.5 * Jf["BTC"] + 0.5 * Jf["ETH"]) fade_d = results[("1h", "prevday", "-")]["comb_daily"] JJ = pd.concat({"fade": fade_d, "follow": follow_d}, axis=1, join="inner").dropna() JH = JJ[JJ.index >= HOLDOUT] JI = JJ[JJ.index < HOLDOUT] print(f" corr daily fade-follow: FULL={JJ['fade'].corr(JJ['follow']):+.3f} " f"HOLD={JH['fade'].corr(JH['follow']):+.3f}") print(f" Sharpe IS : fade={al._sh(JI['fade']):+.2f} follow={al._sh(JI['follow']):+.2f}") print(f" Sharpe HOLD: fade={al._sh(JH['fade']):+.2f} follow={al._sh(JH['follow']):+.2f}") print(" per anno (Sharpe fade | follow):") for y in sorted(set(JJ.index.year)): sub = JJ[JJ.index.year == y] if len(sub) > 40: print(f" {y}: {al._sh(sub['fade']):+.2f} | {al._sh(sub['follow']):+.2f}") # ---------- 10. MARGINAL vs TP01 (solo se il best-IS regge) ---------- if ch["sh_full"] >= 0.5 and ch["sh_is"] >= 0.5: print("\n[9] MARGINAL vs TP01 (best-IS regge >=0.5 -> gate)") m = al.marginal_vs_tp01(ch["comb_daily"]) print(f" verdict={m.get('marginal_verdict')} corr_full={m.get('corr_full')} " f"uplift w25 full={m['blends']['w25']['uplift_full']:+.3f} " f"hold={m['blends']['w25']['uplift_hold']}") print(f" has_insample_edge={m.get('has_insample_edge')} is_hedge={m.get('is_hedge')} " f"robust_oos={m.get('robust_oos')} multicut={m.get('multicut_uplift')}") else: print(f"\n[9] MARGINAL vs TP01: SALTATO — best-IS Sharpe FULL={ch['sh_full']:+.2f} / " f"IS={ch['sh_is']:+.2f} sotto la soglia 0.5 standalone") print("\nFine. Nessun file scritto fuori da questo script; selezione solo in-sample.") if __name__ == "__main__": main()