"""orthogonal_signals.py — SIGNAL-BASED, BY-CONSTRUCTION-ORTHOGONAL streams (2026-06-29). TESI (richiesta utente). Il book attivo è TP01 (trend) + XS01 (cross-sectional) + VRP01 (short-vol) + SKH01 (regime L/S). Tutti hanno beta direzionale o vol crypto. Cerchiamo uno stream a **beta di mercato ~0 e bassa corr al book** ma con un **EDGE DI SEGNALE reale** — NON la "diversification math" di uno stream a Sharpe~0 (il marginal scorer indurito la boccia: serve has_insample_edge, Sharpe standalone PRE-2025 >= 0.5, deflated-Sharpe sull'intera griglia, e selezione della cella IN-SAMPLE, mai sul max hold-out). FOCUS PRIMARIO — RELATIVE-VALUE ETH/BTC (dollar-neutral, 2 gambe). La posizione è sul SPREAD long-ETH/short-BTC (o viceversa): r_spread[i] = pos[i-1]*(r_eth[i]-r_btc[i]) - fee*2*|Δpos|. Per costruzione beta_mercato ~0 -> scorrelato a TP01/SKH. VANTAGGIO: un book a 2 gambe su Deribit perp (BTC+ETH entrambi live) è MOLTO più vicino all'eseguibile a $600 di uno a 19 gambe (XS01 è STAT-MODE). Segnali sul ratio log(ETH/BTC), tutti CAUSALI (decisione <= close[i]): 1. RATIO-MOM momentum del ratio (trend dello spread). 2. RATIO-REV reversal di breve del ratio. 3. RATIO-ACCEL accelerazione (2a differenza / curvatura) del ratio. 4. VOLSPREAD vol realizzata relativa BTC vs ETH -> verso l'asset col profilo giusto. 5. DVOLSPREAD vol IMPLICITA relativa (al.dvol BTC/ETH) -> re-valida l'ex-lead 'dvol_spread'. 6. STATARB-RESID residuo di ETH dopo beta*BTC (rolling OLS causale) -> mean-revert il residuo. SECONDARIO — CRYPTO vs MACRO (GLD/QQQ/TLT): long/short crypto vs hedge su momentum relativo, merge_asof backward (equity 5gg/sett vs crypto 24/7), niente look-ahead. Probabile debole. GATE (tutti obbligatori, replicano altlib indurito): * CAUSALITÀ: prefix-check sul SPREAD (ricostruisci su prefisso, la coda deve combaciare). * NETTO fee 0.10% RT su 2 gambe + SWEEP (0.00-0.30% RT). * SELEZIONE CELLA IN-SAMPLE-ONLY (pre-2025), MAI sul max hold-out (punto cieco filone B). * DEFLATED-SHARPE su TUTTE le celle cercate (multiple-testing). * has_insample_edge: Sharpe standalone PRE-2025 >= 0.5 (no diversification-math). * OOS hold-out 2025+, plateau su griglia, per-anno. * corr vs BOOK 4-sleeve (|corr|<0.2 ideale) + beta vs mercato (50/50 BTC+ETH) ~0. * marginal_vs_tp01 (ADDS / HEDGE / NOISE / NEUTRAL) -> earns_slot_honest. * EXEC $600: haircut a 2 gambe (min $5/gamba, fee 0.10% RT) — il punto FORTE del filone. ONESTÀ BRUTALE: bassa-corr da sola NON basta. dvol_spread era forward-monitor (storia DVOL corta + multiple-testing); ratio_accel era lead debole. Se è diversification-math o hold-out-fitting -> NOISE/SCARTATO con numeri. Esecuzione: cd /opt/docker/PythagorasGoal && uv run python scripts/research/orthogonal_signals.py Idempotente, solo stdout. """ from __future__ import annotations import sys import math import warnings from pathlib import Path import numpy as np import pandas as pd warnings.filterwarnings("ignore") _ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt")) sys.path.insert(0, str(_ROOT)) import altlib as al # noqa: E402 FEE_SIDE = al.FEE_SIDE # 0.0005 = 0.05%/side FEE_SWEEP = (0.0, 0.00025, 0.0005, 0.001, 0.0015) # per-side; ×2 legs ×2 (RT) for RT% HOLDOUT = al.HOLDOUT ANN = 365.25 # =========================================================================== # JOINT FRAME + DOLLAR-NEUTRAL EVALUATOR (custom — eval_weights is single-asset) # =========================================================================== def build_joint(tf: str = "1d") -> pd.DataFrame: """BTC/ETH allineati sull'indice comune (inner join su timestamp). Ritorna un frame con r_btc/r_eth (simple), close_*, log_ratio = log(ETH/BTC), datetime, timestamp.""" b = al.get("BTC", tf)[["timestamp", "datetime", "close"]].rename(columns={"close": "cb"}) e = al.get("ETH", tf)[["timestamp", "close"]].rename(columns={"close": "ce"}) j = b.merge(e, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True) j["r_btc"] = al.simple_returns(j["cb"].values) j["r_eth"] = al.simple_returns(j["ce"].values) j["log_ratio"] = np.log(j["ce"].values / j["cb"].values) return j def spread_ret(j: pd.DataFrame) -> np.ndarray: """Ritorno dollar-neutral per unità di gross-per-gamba: long $1 ETH, short $1 BTC.""" return j["r_eth"].values - j["r_btc"].values def vol_target_spread(direction: np.ndarray, j: pd.DataFrame, target_vol: float = 0.20, win_days: int = 30, cap: float = 2.0) -> np.ndarray: """Scala una direzione in [-1,1] a vol-target sullo SPREAD (causale: vol realizzata <= i).""" s = spread_ret(j) bpd = al.bars_per_day(j) bpy = bpd * ANN vol = al.realized_vol(s, max(2, win_days * bpd), bpy) scal = np.where((vol > 0) & np.isfinite(vol), target_vol / vol, 0.0) pos = np.clip(np.nan_to_num(direction) * scal, -cap, cap) pos[~np.isfinite(pos)] = 0.0 return pos def eval_spread(j: pd.DataFrame, pos: np.ndarray, fee_side: float = FEE_SIDE) -> dict: """Backtest ONESTO del SPREAD. pos[i] decisa <= close[i], TENUTA durante la barra i+1 (lo shift è qui -> niente leak). Fee su 2 GAMBE: ogni Δpos muove ETH e BTC -> 2×|Δpos|.""" pos = np.nan_to_num(np.asarray(pos, float)) s = spread_ret(j) held = np.zeros(len(pos)); held[1:] = pos[:-1] gross = held * s turn = np.abs(np.diff(held, prepend=0.0)) # turnover per-gamba net = gross - fee_side * 2.0 * turn # 2 gambe net[0] = 0.0 idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) full = al._metrics_from_net(net, idx) hmask = idx >= HOLDOUT hold = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 3 else dict(sharpe=0.0, n=0) return dict(full=full, holdout=hold, yearly=al._yearly(net, idx), tim=round(float(np.mean(held != 0)), 3), turnover=round(float(turn.sum() / (len(turn) / (al.bars_per_day(j) * ANN))), 1), net=net, idx=idx) def spread_daily(j: pd.DataFrame, pos: np.ndarray, fee_side: float = FEE_SIDE) -> pd.Series: """Serie NET giornaliera del candidato spread (compounded a 1d se TF sub-daily).""" ev = eval_spread(j, pos, fee_side=fee_side) s = pd.Series(ev["net"], index=ev["idx"]) return al._to_daily(s) def eval_spread_smallcap(j: pd.DataFrame, pos: np.ndarray, capital: float = 600.0, min_order: float = 5.0, fee_side: float = FEE_SIDE) -> dict: """Net REALISTICO a $600 su 2 GAMBE. Un Δpos il cui nozionale PER-GAMBA |Δpos|*capital < $5 NON si esegue (held). Le due gambe cambiano dello stesso |Δ| -> il vincolo binding è |Δpos|*capital >= min_order. Riporta Sharpe modellato vs realistico + haircut + n trade.""" pos = np.clip(np.nan_to_num(np.asarray(pos, float)), -10, 10) held = np.empty(len(pos)); cur = 0.0; n_tr = 0 for i in range(len(pos)): if abs(pos[i] - cur) * capital >= min_order: cur = pos[i]; n_tr += 1 held[i] = cur s = spread_ret(j) p = np.zeros(len(held)); p[1:] = held[:-1] turn = np.abs(np.diff(p, prepend=0.0)) net = p * s - fee_side * 2.0 * turn; net[0] = 0.0 idx = pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)) real = al._metrics_from_net(net, idx) modeled = eval_spread(j, pos, fee_side=fee_side)["full"] return dict(realistic=real, modeled=modeled, sharpe_haircut=round(modeled["sharpe"] - real["sharpe"], 3), n_executed_trades=int(n_tr)) # =========================================================================== # CAUSALITY — prefix-check sul SPREAD (la coda del prefisso deve combaciare col full). # =========================================================================== def causality_spread(make_pos, tf: str = "1d", tail: int = 80, tol: float = 1e-6) -> dict: """make_pos(j) -> pos sull'intero frame. Ricostruisce su prefissi; la coda deve combaciare.""" j = build_joint(tf) full = np.nan_to_num(make_pos(j)) n = len(j) worst = 0.0; bad = False; checked = 0 for cut in (int(n * 0.80), int(n * 0.92)): if cut <= tail + 5 or cut >= n: continue sub = j.iloc[:cut].reset_index(drop=True) s = np.nan_to_num(make_pos(sub)) if len(s) != cut: bad = True; continue d = np.abs(s[cut - tail:cut] - full[cut - tail:cut]) worst = max(worst, float(np.max(d)) if len(d) else 0.0) checked += 1 return dict(ok=bool((not bad) and worst <= tol), max_tail_diff=round(worst, 10), checked=checked) # =========================================================================== # SIGNAL FACTORIES — factory(tf, **p) -> make_pos(j) -> vol-targeted position on the spread # (direzione in [-1,1] internamente, poi vol_target_spread). 'sgn' (+/-1) testa il verso. # =========================================================================== def f_ratio_mom(tf="1d", L=30, sgn=1, tv=0.20, vw=30, cap=2.0): def make(j): lr = j["log_ratio"].values mom = lr - pd.Series(lr).shift(L).values # momentum L-barre del ratio z = al.zscore(mom, max(20, L)) d = sgn * np.tanh(np.nan_to_num(z)) return vol_target_spread(d, j, tv, vw, cap) return make def f_ratio_rev(tf="1d", L=5, sgn=-1, tv=0.20, vw=30, cap=2.0): def make(j): lr = j["log_ratio"].values z = al.zscore(lr, L) # deviazione di breve dal trend locale d = sgn * np.tanh(np.nan_to_num(z)) return vol_target_spread(d, j, tv, vw, cap) return make def f_ratio_accel(tf="1d", L=20, sgn=-1, tv=0.20, vw=30, cap=2.0): def make(j): lr = pd.Series(j["log_ratio"].values) accel = lr - 2 * lr.shift(L) + lr.shift(2 * L) # 2a differenza (curvatura) z = al.zscore(np.nan_to_num(accel.values), max(20, L)) d = sgn * np.tanh(np.nan_to_num(z)) return vol_target_spread(d, j, tv, vw, cap) return make def f_volspread(tf="1d", W=30, sgn=1, tv=0.20, vw=30, cap=2.0): def make(j): bpd = al.bars_per_day(j); bpy = bpd * ANN vb = al.realized_vol(j["r_btc"].values, max(2, W * bpd), bpy) ve = al.realized_vol(j["r_eth"].values, max(2, W * bpd), bpy) z = al.zscore(np.nan_to_num(vb - ve), max(30, W)) # BTC più volatile di ETH? d = sgn * np.tanh(np.nan_to_num(z)) return vol_target_spread(d, j, tv, vw, cap) return make def f_dvolspread(tf="1d", W=30, sgn=1, tv=0.20, vw=30, cap=2.0): def make(j): db = al.dvol(j, "BTC"); de = al.dvol(j, "ETH") # vol IMPLICITA causale (merge_asof) sp = np.nan_to_num(db - de) z = al.zscore(sp, max(30, W)) d = sgn * np.tanh(np.nan_to_num(z)) return vol_target_spread(d, j, tv, vw, cap) return make def f_statarb_resid(tf="1d", W=60, sgn=-1, tv=0.20, vw=30, cap=2.0): def make(j): x = np.log(j["cb"].values); y = np.log(j["ce"].values) # OLS rolling causale y~a+b x sx = pd.Series(x); sy = pd.Series(y) mx = sx.rolling(W, min_periods=W).mean() my = sy.rolling(W, min_periods=W).mean() cov = (sx * sy).rolling(W, min_periods=W).mean() - mx * my var = (sx * sx).rolling(W, min_periods=W).mean() - mx * mx beta = (cov / var.replace(0, np.nan)) resid = (sy - (my - beta * mx) - beta * sx).values # resid al tempo i (usa <= i) z = al.zscore(np.nan_to_num(resid), W) d = sgn * np.tanh(np.nan_to_num(z)) # mean-revert il residuo return vol_target_spread(d, j, tv, vw, cap) return make FAMILIES = { "RATIO-MOM": (f_ratio_mom, [dict(L=L, sgn=s) for L in (15, 30, 45, 60, 90) for s in (1, -1)], ("1d",)), "RATIO-REV": (f_ratio_rev, [dict(L=L, sgn=s) for L in (3, 5, 8, 12, 20) for s in (1, -1)], ("1d",)), "RATIO-ACCEL": (f_ratio_accel, [dict(L=L, sgn=s) for L in (10, 20, 30, 45) for s in (1, -1)], ("1d",)), "VOLSPREAD": (f_volspread, [dict(W=W, sgn=s) for W in (10, 20, 30, 60) for s in (1, -1)], ("1d",)), "DVOLSPREAD": (f_dvolspread, [dict(W=W, sgn=s) for W in (15, 30, 45, 60) for s in (1, -1)], ("1d",)), "STATARB-RESID": (f_statarb_resid, [dict(W=W, sgn=s) for W in (30, 45, 60, 90, 120) for s in (1, -1)], ("1d",)), } # =========================================================================== # BOOK + MARKET references (for corr / beta) # =========================================================================== def book_daily() -> pd.Series: """Serie NET giornaliera del BOOK attivo a 4 sleeve (TP01+XS01+VRP01+SKH01).""" from src.portfolio.sleeves import active_sleeves from src.portfolio.portfolio import StrategyPortfolio pf = StrategyPortfolio(active_sleeves()); pf.backtest() s = pf.combined_daily() if not isinstance(s.index, pd.DatetimeIndex): s.index = pd.to_datetime(s.index, utc=True) if s.index.tz is None: s.index = s.index.tz_localize("UTC") return s.dropna() def market_daily() -> pd.Series: """Mercato di riferimento: 50/50 BTC+ETH ritorni semplici giornalieri (per beta ~0).""" series = {} for a in ("BTC", "ETH"): df = al.get(a, "1d") series[a] = pd.Series(al.simple_returns(df["close"].values), index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))) J = pd.concat(series, axis=1, join="inner").fillna(0.0) return (0.5 * J["BTC"] + 0.5 * J["ETH"]).dropna() def beta_to(cand: pd.Series, ref: pd.Series) -> tuple[float, float]: J = pd.concat({"c": cand, "r": ref}, axis=1, join="inner").dropna() if len(J) < 30 or J["r"].var() == 0: return float("nan"), float("nan") c, r = J["c"].values, J["r"].values beta = float(np.cov(c, r)[0, 1] / np.var(r)) return round(beta, 3), round(float(J["c"].corr(J["r"])), 3) # =========================================================================== # HONEST FAMILY STUDY for the SPREAD (replica study_family_honest a mano): # (1) cella scelta su Sharpe IN-SAMPLE (pre-HOLDOUT), MAI sul max hold-out; # (2) deflated-Sharpe su TUTTE le celle; (3) marginal_vs_tp01 sulla cella scelta. # =========================================================================== def _sh(s) -> float: return al._sh(s) def study_spread_family(name, factory, grid, tfs, refs, fee_side=FEE_SIDE, dsr_min=0.95) -> dict: rows = [] for tf in tfs: for p in grid: try: j = build_joint(tf) pos = factory(tf=tf, **p)(j) daily = spread_daily(j, pos, fee_side=fee_side) except Exception as ex: # pragma: no cover rows.append(dict(tf=tf, params=p, err=str(ex)[:60])); continue ins = daily[daily.index < HOLDOUT] is_sh = _sh(ins) if len(ins) > 60 else float("nan") rows.append(dict(tf=tf, params=p, daily=daily, insample_sharpe=round(is_sh, 3), full_sharpe=round(_sh(daily), 3))) valid = [r for r in rows if r.get("insample_sharpe") is not None and np.isfinite(r.get("insample_sharpe", np.nan))] if not valid: return dict(name=name, chosen=None, earns_slot_honest=False, reason="no valid in-sample cell") chosen = max(valid, key=lambda r: r["insample_sharpe"]) all_full = [r["full_sharpe"] for r in rows if r.get("full_sharpe") is not None] j = build_joint(chosen["tf"]) pos = factory(tf=chosen["tf"], **chosen["params"])(j) daily = chosen["daily"] ev = eval_spread(j, pos, fee_side=fee_side) # fee sweep (RT% = per-side ×2 legs? No: RT per leg = 2×side; on 2 legs the cost already # ×2 in eval_spread. Report per-side grid -> "X%RT/leg".) sweep = {} for f in FEE_SWEEP: sweep[f"{2*f*100:.2f}%RT/leg"] = round(eval_spread(j, pos, fee_side=f)["full"]["sharpe"], 3) fee_survives = sweep.get(f"{2*0.0015*100:.2f}%RT/leg", -9) > 0 # marginal vs TP01 marg = al.marginal_vs_tp01(daily) # deflated Sharpe over the WHOLE grid (come da istruzione) dsr, sr0 = al.deflated_sharpe(_sh(daily), all_full, daily) dsr_pass = bool(np.isfinite(dsr) and dsr >= dsr_min) # SENSIBILITÀ: la griglia include sgn=+1 E sgn=-1 (specchi: +Sh e -Sh) -> raddoppia la # dispersione dei trial e gonfia il null-max. DSR "same-sign" = deflazione sul SOLO verso # scelto (conta i soli lookback davvero in competizione). È il limite ottimistico onesto. same = [r["full_sharpe"] for r in rows if r.get("full_sharpe") is not None and r["params"].get("sgn") == chosen["params"].get("sgn")] dsr_ss, sr0_ss = al.deflated_sharpe(_sh(daily), same if len(same) >= 2 else all_full, daily) dsr_ss_pass = bool(np.isfinite(dsr_ss) and dsr_ss >= dsr_min) # corr/beta vs BOOK & MARKET bbeta, bcorr = beta_to(daily, refs["book"]) mbeta, mcorr = beta_to(daily, refs["market"]) btcbeta, btccorr = beta_to(daily, refs["btc"]) ethbeta, ethcorr = beta_to(daily, refs["eth"]) # $600 executability (2 legs) sc = eval_spread_smallcap(j, pos) earns = bool(marg.get("marginal_verdict") == "ADDS" and marg.get("robust_oos", False) and marg.get("has_insample_edge", False) and not marg.get("is_hedge", False) and dsr_pass and fee_survives) return dict(name=name, n_cells=len(all_full), chosen=chosen, rows=valid, ev=ev, sweep=sweep, fee_survives=fee_survives, marginal=marg, deflated_sharpe=round(dsr, 3) if np.isfinite(dsr) else None, expected_null_max=round(sr0, 3) if np.isfinite(sr0) else None, dsr_pass=dsr_pass, dsr_samesign=round(dsr_ss, 3) if np.isfinite(dsr_ss) else None, expected_null_max_ss=round(sr0_ss, 3) if np.isfinite(sr0_ss) else None, dsr_ss_pass=dsr_ss_pass, n_cells_samesign=len(same), book=dict(beta=bbeta, corr=bcorr), market=dict(beta=mbeta, corr=mcorr), btc=dict(beta=btcbeta, corr=btccorr), eth=dict(beta=ethbeta, corr=ethcorr), smallcap=sc, earns_slot_honest=earns) def verdict_for(rep: dict) -> tuple[str, str]: if rep.get("chosen") is None: return "SCARTATO", "nessuna cella valida" m = rep["marginal"] is_edge = m.get("has_insample_edge"); is_sh = m.get("cand_insample_sharpe") dsr = rep.get("deflated_sharpe"); mv = m.get("marginal_verdict") ss = rep.get("dsr_samesign"); ss_tag = f"same-sign {ss} {'pass' if rep.get('dsr_ss_pass') else 'fail'}" if rep["earns_slot_honest"]: return "SLEEVE-CANDIDATE-eseguibile", "ADDS + robust_oos + edge in-sample + DSR pass + fee/exec ok" if mv == "DILUTES": return "SCARTATO", "DILUTES (abbassa il blend del book)" if m.get("is_hedge"): return "SCARTATO", "HEDGE (paga solo quando TP01 è debole, non alpha)" if not is_edge: return "NOISE", f"no edge in-sample (Sharpe<2025 {is_sh} < 0.5) -> diversification-math" # ha edge in-sample reale: il discriminante è se MIGLIORA il book (marginal ADDS) if mv == "ADDS": if not rep.get("dsr_pass"): return "LEAD-forward-monitor", (f"ADDS + edge in-sample {is_sh} + executable, MA deflated-Sharpe " f"full-grid {dsr} < 0.95 ({ss_tag}) -> multiple-testing") if not m.get("robust_oos"): return "LEAD-forward-monitor", "ADDS + edge ma non robust_oos (single-window / multi-cut debole)" return "LEAD-forward-monitor", "ADDS ma blocco fee/exec" # edge standalone reale ma marginal NEUTRAL/REDUNDANT: NON migliora il book -> niente slot return "NEUTRAL-standalone", (f"edge in-sample {is_sh} reale ma marginal={mv}: non migliora il book " f"(corr~0 senza uplift = diversification-math; hold-out debole)") def print_family(rep: dict): print("=" * 100) print(f"### {rep['name']}") if rep.get("chosen") is None: print(" SCARTATO:", rep.get("reason")); return ch = rep["chosen"]; ev = rep["ev"]; m = rep["marginal"] print(f" best cell (IN-SAMPLE pick): tf={ch['tf']} params={ch['params']} " f"[cercate {rep['n_cells']} celle]") print(f" in-sample Sharpe (pre-2025) {ch['insample_sharpe']} | FULL Sharpe {ev['full']['sharpe']} " f"DD {ev['full']['maxdd']*100:.1f}% ret {ev['full']['ret']*100:+.0f}% | " f"HOLD Sharpe {ev['holdout'].get('sharpe')} ret {ev['holdout'].get('ret',0)*100:+.0f}%") print(f" time-in-mkt {ev['tim']} turnover/yr {ev['turnover']}") print(f" per-anno: " + " ".join(f"{y}:{d['ret']*100:+.0f}%(dd{d['dd']*100:.0f})" for y, d in ev["yearly"].items())) print(f" fee sweep (per-leg RT): {rep['sweep']} fee_survives={rep['fee_survives']}") print(f" corr vs BOOK {rep['book']['corr']} (beta {rep['book']['beta']}) | " f"beta vs MERCATO(50/50) {rep['market']['beta']} (corr {rep['market']['corr']})") print(f" corr/beta vs BTC {rep['btc']['corr']}/{rep['btc']['beta']} " f"vs ETH {rep['eth']['corr']}/{rep['eth']['beta']}") print(f" MARGINAL vs TP01: verdict={m.get('marginal_verdict')} corr_full={m.get('corr_full')} " f"corr_hold={m.get('corr_hold')}") print(f" has_insample_edge={m.get('has_insample_edge')} (standalone Sh<2025 {m.get('cand_insample_sharpe')}) " f"robust_oos={m.get('robust_oos')} multicut={m.get('multicut_persistent')} {m.get('multicut_uplift')}") print(f" blend w25: full {m['blends']['w25']['full']} (uplift {m['blends']['w25']['uplift_full']:+.3f}) " f"hold {m['blends']['w25']['hold']} (uplift {m['blends']['w25']['uplift_hold']}) is_hedge={m.get('is_hedge')}") print(f" DEFLATED-Sharpe full-grid {rep['deflated_sharpe']} (null-max {rep['expected_null_max']}, " f"{rep['n_cells']} celle) pass={rep['dsr_pass']} | same-sign {rep['dsr_samesign']} " f"(null-max {rep['expected_null_max_ss']}, {rep['n_cells_samesign']} celle) pass={rep['dsr_ss_pass']}") sc = rep["smallcap"] print(f" EXEC $600 (2 gambe): modeled Sh {sc['modeled']['sharpe']} -> realistic {sc['realistic']['sharpe']} " f"(haircut {sc['sharpe_haircut']}) trade eseguiti {sc['n_executed_trades']}") v, why = verdict_for(rep) print(f" >>> VERDETTO: {v} — {why}") print(f" earns_slot_honest = {rep['earns_slot_honest']}") # =========================================================================== # SECONDARIO — CRYPTO vs MACRO (GLD/QQQ/TLT) momentum relativo, merge_asof backward. # =========================================================================== def _eq_daily(sym: str) -> pd.Series: p = _ROOT / "data" / "raw" / f"eq_{sym}_1d.parquet" d = pd.read_parquet(p).sort_values("timestamp").reset_index(drop=True) idx = pd.DatetimeIndex(pd.to_datetime(d["timestamp"], unit="ms", utc=True)) return pd.Series(d["close"].values, index=idx) def study_macro_rv(hedge: str, L: int = 60, sgn: int = 1, fee_side: float = FEE_SIDE, refs: dict | None = None) -> dict: """Long crypto (50/50 BTC+ETH) / short hedge ETF su momentum relativo L-giorni. Allineamento: calendario dell'EQUITY (trading days); crypto allineata backward (no look-ahead). Decisione <= close[i] del giorno di trading.""" eqc = _eq_daily(hedge) # crypto close 50/50 (geometric proxy: media dei log-prezzi normalizzati) cb = al.get("BTC", "1d"); ce = al.get("ETH", "1d") cbi = pd.Series(cb["close"].values, index=pd.DatetimeIndex(pd.to_datetime(cb["datetime"], utc=True))) cei = pd.Series(ce["close"].values, index=pd.DatetimeIndex(pd.to_datetime(ce["datetime"], utc=True))) cj = pd.concat({"b": cbi, "e": cei}, axis=1, join="inner").dropna() crypto = np.exp(0.5 * np.log(cj["b"]) + 0.5 * np.log(cj["e"])) # crypto index level # merge_asof backward: per ogni giorno equity, l'ultimo close crypto <= quel giorno L_ = pd.DataFrame({"ts": eqc.index, "eq": eqc.values}).sort_values("ts") R_ = pd.DataFrame({"ts": crypto.index, "cr": crypto.values}).sort_values("ts") mg = pd.merge_asof(L_, R_, on="ts", direction="backward").dropna() eqv = mg["eq"].values; crv = mg["cr"].values; idx = pd.DatetimeIndex(mg["ts"]) r_eq = al.simple_returns(eqv); r_cr = al.simple_returns(crv) s = r_cr - r_eq # long crypto / short hedge lr = np.log(crv) - np.log(eqv) # log relative level mom = lr - pd.Series(lr).shift(L).values z = al.zscore(np.nan_to_num(mom), max(20, L)) d = sgn * np.tanh(np.nan_to_num(z)) # vol target on the macro spread vol = al.realized_vol(s, 30, ANN) scal = np.where((vol > 0) & np.isfinite(vol), 0.20 / vol, 0.0) pos = np.clip(np.nan_to_num(d) * scal, -2.0, 2.0) held = np.zeros(len(pos)); held[1:] = pos[:-1] net = held * s - fee_side * 2.0 * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 full = al._metrics_from_net(net, idx) hmask = idx >= HOLDOUT hold = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 3 else dict(sharpe=0.0, n=0) daily = al._to_daily(pd.Series(net, index=idx)) ins = daily[daily.index < HOLDOUT] is_sh = round(_sh(ins), 3) if len(ins) > 60 else None bcorr = beta_to(daily, refs["book"])[1] if refs else None mbeta, mcorr = beta_to(daily, refs["market"]) if refs else (None, None) btcb = beta_to(daily, refs["btc"])[0] if refs else None return dict(hedge=hedge, L=L, sgn=sgn, full=full, holdout=hold, insample_sharpe=is_sh, book_corr=bcorr, mkt_beta=mbeta, mkt_corr=mcorr, btc_beta=btcb, yearly=al._yearly(net, idx), daily=daily) # =========================================================================== # MAIN # =========================================================================== def main(): print("#" * 100) print("# ORTHOGONAL SIGNALS — RELATIVE-VALUE ETH/BTC (dollar-neutral) + CRYPTO-vs-MACRO") print("#" * 100) j = build_joint("1d") print(f"Joint BTC/ETH 1d: {len(j)} barre {j['datetime'].min().date()} -> {j['datetime'].max().date()} " f"(hold-out {HOLDOUT.date()}+)") s = spread_ret(j) print(f"Spread r_eth-r_btc: vol annua {np.std(s)*math.sqrt(ANN)*100:.0f}% " f"corr(r_eth,r_btc)={np.corrcoef(j['r_eth'].values[1:], j['r_btc'].values[1:])[0,1]:.2f}") # references print("\nCarico riferimenti BOOK / MERCATO ...") refs = dict(book=book_daily(), market=market_daily()) cb = al.get("BTC", "1d"); ce = al.get("ETH", "1d") refs["btc"] = pd.Series(al.simple_returns(cb["close"].values), index=pd.DatetimeIndex(pd.to_datetime(cb["datetime"], utc=True))) refs["eth"] = pd.Series(al.simple_returns(ce["close"].values), index=pd.DatetimeIndex(pd.to_datetime(ce["datetime"], utc=True))) print(f" BOOK 4-sleeve: {len(refs['book'])} giorni, Sharpe {_sh(refs['book']):.2f}") # causality smoke-test on one cell per family print("\n--- CAUSALITÀ (prefix-check sullo SPREAD, tol 1e-6) ---") for name, (fac, grid, tfs) in FAMILIES.items(): p0 = grid[0] ck = causality_spread(fac(tf=tfs[0], **p0), tf=tfs[0]) print(f" {name:14s} ok={ck['ok']} max_tail_diff={ck['max_tail_diff']} checked={ck['checked']}") # study each family print("\n" + "#" * 100) print("# RELATIVE-VALUE ETH/BTC — selezione cella IN-SAMPLE, deflated-Sharpe su tutta la griglia") print("#" * 100) summary = [] for name, (fac, grid, tfs) in FAMILIES.items(): rep = study_spread_family(name, fac, grid, tfs, refs) print_family(rep) v, why = verdict_for(rep) ch = rep.get("chosen") summary.append(dict(name=name, verdict=v, cell=(ch["params"] if ch else None), full=rep["ev"]["full"]["sharpe"] if ch else None, hold=rep["ev"]["holdout"].get("sharpe") if ch else None, is_sh=rep["marginal"].get("cand_insample_sharpe") if ch else None, book_corr=rep["book"]["corr"] if ch else None, mkt_beta=rep["market"]["beta"] if ch else None, dsr=rep.get("deflated_sharpe") if ch else None, marginal=rep["marginal"].get("marginal_verdict") if ch else None, earns=rep.get("earns_slot_honest") if ch else False, haircut=rep["smallcap"]["sharpe_haircut"] if ch else None)) # secondary: crypto vs macro print("\n" + "#" * 100) print("# SECONDARIO — CRYPTO vs MACRO (relative momentum, merge_asof backward)") print("#" * 100) macro_rows = [] for hedge in ("gld", "qqq", "tlt"): best = None for L in (30, 60, 90): for sgn in (1, -1): r = study_macro_rv(hedge, L=L, sgn=sgn, refs=refs) if best is None or (r["insample_sharpe"] or -9) > (best["insample_sharpe"] or -9): best = r macro_rows.append(best) print(f" {hedge.upper():4s} bestIS L={best['L']} sgn={best['sgn']}: " f"FULL Sh {best['full']['sharpe']} DD {best['full']['maxdd']*100:.0f}% " f"HOLD Sh {best['holdout'].get('sharpe')} in-sample Sh {best['insample_sharpe']} " f"corr->BOOK {best['book_corr']} beta->MERCATO {best['mkt_beta']} (corr {best['mkt_corr']}) " f"beta->BTC {best['btc_beta']}") print(" NOTA: la vol crypto (~47%) domina GLD/TLT/QQQ (~15%) -> r_cr-r_hedge ≈ r_cr: la 'relative" " momentum' è di fatto MOMENTUM CRYPTO (la gamba hedge è troppo poco volatile per neutralizzare).") print(" Beta-mercato ~0 solo perché il momentum entra/esce dall'esposizione; ma la corr->BOOK" " 0.17-0.20 (vs ~0.02 degli spread ETH/BTC) tradisce l'overlap col trend di TP01 -> NON ortogonale.") # marginal gate onesto sul migliore macro (per smentire il numero di Sharpe tentatore) bm = max(macro_rows, key=lambda r: (r["insample_sharpe"] or -9)) mg = al.marginal_vs_tp01(bm["daily"]) print(f" MARGINAL gate sul migliore ({bm['hedge'].upper()}): verdict={mg.get('marginal_verdict')} " f"corr_full={mg.get('corr_full')} blend-uplift w25 full {mg['blends']['w25']['uplift_full']:+.3f} " f"hold {mg['blends']['w25']['uplift_hold']} robust_oos={mg.get('robust_oos')} " f"-> {'overlap col trend (no slot)' if mg.get('marginal_verdict') in ('NEUTRAL','REDUNDANT','DILUTES') else 'da gate completo'}") # final table print("\n" + "#" * 100) print("# SINTESI") print("#" * 100) print(f"{'segnale':14s} {'verdetto':28s} {'FULL':>5s} {'HOLD':>5s} {'IS-Sh':>5s} " f"{'corrBK':>6s} {'mβ':>5s} {'DSR':>5s} {'marg':>9s} {'cut':>6s} earns") for r in summary: print(f"{r['name']:14s} {r['verdict']:28s} " f"{str(r['full']):>5s} {str(r['hold']):>5s} {str(r['is_sh']):>5s} " f"{str(r['book_corr']):>6s} {str(r['mkt_beta']):>5s} {str(r['dsr']):>5s} " f"{str(r['marginal']):>9s} {str(r['haircut']):>6s} {r['earns']}") print("\nMACRO (secondario): " + " | ".join( f"{m['hedge'].upper()} IS={m['insample_sharpe']} HOLD={m['holdout'].get('sharpe')} bkcorr={m['book_corr']}" for m in macro_rows)) winners = [r for r in summary if r["earns"]] leads = [r for r in summary if r["verdict"].startswith("LEAD")] standalone = [r for r in summary if r["verdict"].startswith("NEUTRAL")] print("\n" + "=" * 100) print("CONCLUSIONE — c'è uno stream scorrelato CON edge reale ED eseguibile a 2 gambe?") print("=" * 100) if winners: print("SI -> SLEEVE-CANDIDATE eseguibile a 2 gambe:", ", ".join(r["name"] for r in winners)) else: print("NO sleeve pronto. L'ORTOGONALITÀ c'è (corr->BOOK ~0.02, beta-mercato ~0.01: il filone") print("relative-value ETH/BTC è scorrelato per costruzione, ed è ESEGUIBILE a $600 — haircut ~0,") print("fee-surviving a 0.30%RT/gamba). Ma l'EDGE non passa il deflated-Sharpe:") if leads: print(" LEAD (forward-monitor, ADDS + edge in-sample ma DSR<0.95):", ", ".join(r["name"] for r in leads)) if standalone: print(" NEUTRAL-standalone (edge reale, NON migliora il book):", ", ".join(r["name"] for r in standalone)) print("Dettaglio nei verdetti per-segnale sopra.") if __name__ == "__main__": main()