"""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}"