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:
@@ -0,0 +1,83 @@
|
||||
"""Test minimali per orthogonal_signals.py — CAUSALITÀ dollar-neutral + dollar-neutrality (beta~0).
|
||||
|
||||
Lo scopo è blindare le due proprietà su cui poggia tutto il filone relative-value ETH/BTC:
|
||||
1. l'evaluator dollar-neutral è CAUSALE: pos[i] decisa a close[i] è tenuta SOLO durante la
|
||||
barra i+1 -> una decisione presa all'ultima barra non può toccare il backtest (no look-ahead),
|
||||
e il prefix-check sul segnale combacia con la coda del full.
|
||||
2. la fee è caricata su 2 GAMBE (ETH + BTC).
|
||||
3. dollar-neutrality: un segnale temporizzato sul ratio ha beta di mercato ~0 (ortogonale per
|
||||
costruzione) — il cuore della richiesta (stream scorrelato al book direzionale).
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
import orthogonal_signals as o # noqa: E402
|
||||
|
||||
|
||||
def _synthetic_joint(n: int = 60, seed: int = 0) -> pd.DataFrame:
|
||||
ts = pd.date_range("2022-01-01", periods=n, freq="D", tz="UTC")
|
||||
rng = np.random.default_rng(seed)
|
||||
cb = 100 * np.cumprod(1 + rng.normal(0, 0.02, n))
|
||||
ce = 100 * np.cumprod(1 + rng.normal(0, 0.03, n))
|
||||
j = pd.DataFrame({"timestamp": ts.view("int64") // 10**6, "datetime": ts, "cb": cb, "ce": ce})
|
||||
j["r_btc"] = o.al.simple_returns(cb)
|
||||
j["r_eth"] = o.al.simple_returns(ce)
|
||||
j["log_ratio"] = np.log(ce / cb)
|
||||
return j
|
||||
|
||||
|
||||
def test_position_held_next_bar_only():
|
||||
"""Una posizione nota a close[k] muove SOLO il ritorno della barra k+1 (eseguibile, no leak)."""
|
||||
j = _synthetic_joint()
|
||||
k = 10
|
||||
pos = np.zeros(len(j)); pos[k] = 1.0
|
||||
ev = o.eval_spread(j, pos, fee_side=0.0)
|
||||
nz = np.nonzero(np.abs(ev["net"]) > 1e-12)[0]
|
||||
assert list(nz) == [k + 1], f"posizione a k={k} deve toccare solo k+1, trovato {nz}"
|
||||
expected = j["r_eth"].values[k + 1] - j["r_btc"].values[k + 1]
|
||||
assert abs(ev["net"][k + 1] - expected) < 1e-12
|
||||
|
||||
|
||||
def test_last_bar_decision_cannot_leak():
|
||||
"""Una decisione presa SOLO all'ultima barra non può influenzare il backtest (è tenuta su una
|
||||
barra i+1 che non esiste) -> net identicamente 0. Guardia anti-look-ahead strutturale."""
|
||||
j = _synthetic_joint()
|
||||
pos = np.zeros(len(j)); pos[-1] = 9.0
|
||||
ev = o.eval_spread(j, pos, fee_side=0.0)
|
||||
assert np.allclose(ev["net"], 0.0)
|
||||
|
||||
|
||||
def test_fee_charged_on_two_legs():
|
||||
"""La fee è su 2 gambe: ogni Δpos paga fee_side su ETH E su BTC -> costo totale = fee*2*turnover."""
|
||||
j = _synthetic_joint()
|
||||
pos = np.zeros(len(j)); pos[5] = 1.0 # held: entra a 6 (Δ=1), esce a 7 (Δ=1) -> turnover=2
|
||||
f = 0.001
|
||||
ev0 = o.eval_spread(j, pos, fee_side=0.0)
|
||||
evf = o.eval_spread(j, pos, fee_side=f)
|
||||
total_fee = float((ev0["net"] - evf["net"]).sum())
|
||||
assert abs(total_fee - f * 2 * 2) < 1e-12, total_fee
|
||||
|
||||
|
||||
def test_prefix_causality_real_signal():
|
||||
"""Prefix-check su dati reali: ricostruendo il segnale su un prefisso, la coda combacia col full."""
|
||||
ck = o.causality_spread(o.f_statarb_resid(W=60), tf="1d")
|
||||
assert ck["ok"] and ck["checked"] >= 1, ck
|
||||
ck2 = o.causality_spread(o.f_ratio_mom(L=30), tf="1d")
|
||||
assert ck2["ok"], ck2
|
||||
|
||||
|
||||
def test_dollar_neutral_low_market_beta():
|
||||
"""Dollar-neutrality: un segnale temporizzato sul ratio ha beta di mercato (50/50 BTC+ETH) ~0.
|
||||
È la proprietà 'ortogonale per costruzione' richiesta dallo studio."""
|
||||
j = o.build_joint("1d")
|
||||
pos = o.f_statarb_resid(W=60)(j)
|
||||
daily = o.spread_daily(j, pos)
|
||||
mkt = o.market_daily()
|
||||
beta, corr = o.beta_to(daily, mkt)
|
||||
assert abs(beta) < 0.10, f"beta di mercato non ~0: {beta}"
|
||||
assert abs(corr) < 0.20, f"corr di mercato troppo alta: {corr}"
|
||||
Reference in New Issue
Block a user