research(sweep): 5 thread paralleli — 0 nuovi sleeve, STATARB-RESID LEAD ortogonale+eseguibile

Ricerca onesta su aree inesplorate (harness altlib+xsec_v2_nonmom, tutti i gate incl.
study_family_honest anti-selection-on-holdout). Branch main, nessun impatto live, test 143/143.

1 XSEC low-risk cousins (MAX/idio-vol/Amihud) -> 1 LEAD (IVOL), STAT-MODE, DSR 0.37<0.95
2 XSEC momentum-structure vs XS01            -> tutto REDUNDANT (sostituire XS01 distrugge hold)
3 Meta-allocazione dinamica (4 sleeve)       -> pesi fissi vincono (gia quasi risk-parity)
4 Segnali ortogonali ETH/BTC (2 gambe)       -> STATARB-RESID + DVOLSPREAD LEAD
5 1-gamba a segnale (MACD/RSI/Supertrend/...) -> 0/12 earns_slot (trend=TP01, MR morta, hedge)

LEAD principale STATARB-RESID (mean-rev residuo ETH-b*BTC, OLS rolling, 2 gambe): primo stream
INSIEME ortogonale (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600 (haircut ~0, NON
STAT-MODE) -> cadono i 2 muri di XS01/opzioni. Resta solo il muro dell'edge (Sharpe 0.84,
DSR 0.929 same-sign <0.95). Causalita+fee verificate dal coordinatore. Forward-monitor, non sleeve.

Soffitto direzionale ~1.3 riconfermato. Diario 2026-06-29-strategy-search-5threads.md, CLAUDE.md agg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-29 20:50:33 +00:00
parent 02d875b07b
commit cff5fa2bf5
12 changed files with 2692 additions and 0 deletions
+604
View File
@@ -0,0 +1,604 @@
"""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} {'':>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()