"""FORWARD-MONITOR — STATARB-RESID (relative-value ETH/BTC, dollar-neutral 2 gambe), PAPER. NON è esecuzione reale. È il monitoraggio forward-only del LEAD dello sweep 2026-06-29 (thread "segnali ortogonali"): il PRIMO stream visto che è INSIEME ortogonale al book (corr→book 0.027, beta-mkt 0.013) ED eseguibile a $600 (book a 2 gambe BTC+ETH perp, NON STAT-MODE come XS01). Resta sotto la soglia solo sull'EDGE (Sharpe ~0.84, deflated-Sharpe <0.95 su storia ~2.5 anni). Questo monitor accumula la finestra OUT-OF-SAMPLE VERA per decidere se l'edge regge — stesso trattamento di PREVDAY/STA05/XS01 STAT-MODE. Nessun edge creduto prima. ⓘ NATURA DEL SEGNALE (onesto): la cella vincente è W=45 **sgn=+1**, NON sgn=-1. Cioè il residuo di ETH dopo β·BTC (β OLS rolling causale) viene seguito in MOMENTUM (long lo spread quando ETH è ricco vs il suo fit su BTC), non in mean-reversion: le dislocazioni relative ETH-vs-BTC CONTINUANO a 1d (la MR pura, sgn=-1, perde -1.4 in-sample). È relative-MOMENTUM, dollar-neutral → ortogonale al trend assoluto di TP01. CONFIG CONGELATA (dallo sweep, selezione IN-SAMPLE-only, mai sul max hold-out): W=45, sgn=+1, tf=1d, vol-target 20%, leva cap 2x. Riusa il segnale ESATTO di scripts/research/orthogonal_signals.py (nessuna reimplementazione → niente drift). DESIGN (onesto, come paper_prevday): - Legge BTC/ETH 1d certificati (build_joint), calcola il segnale CAUSALE su tutta la storia. - Alla prima esecuzione fissa l'INCEPTION all'ultima barra 1d chiusa (forward-only: lo storico NON entra nel PnL di paper). - Ogni run processa le NUOVE barre 1d chiuse: applica il rendimento dello spread tenuto, addebita le fee su 2 GAMBE (ogni Δpos muove ETH e BTC → 2×|Δpos|), registra i flip di segno. - DUE libri in parallelo (onestà sull'esecuzione a piccolo capitale): * MODELED : capitale nominale $2000, ribilanciamento continuo (fee su ogni |Δpos|). * REAL-$600: capitale reale $600, salta i ribilanci di nozionale-per-gamba < min_order ($5) → il gap MODELED-REAL = l'haircut di fill reale (qui ~0: turnover bassissimo a 1d). - Per barre fresche aggiornare prima i dati: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH Stato: data/paper_statarb/{state.json, trades.jsonl, returns.jsonl} (append-only). uv run python scripts/live/paper_statarb.py # avanza col dato disponibile uv run python scripts/live/paper_statarb.py --status # solo stato, non avanza uv run python scripts/live/paper_statarb.py --reset # azzera (riparte da ora) """ from __future__ import annotations import argparse import json import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) # Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest). from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402 STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb" STATE_FILE = STATE_DIR / "state.json" TRADES_FILE = STATE_DIR / "trades.jsonl" RETURNS_FILE = STATE_DIR / "returns.jsonl" # --- CONFIG CONGELATA (frozen) ----------------------------------------------------------- W_FROZEN = 45 SGN_FROZEN = +1 FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker) MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo REAL_CAPITAL = 600.0 # capitale mainnet reale MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia def _signal(j: pd.DataFrame): """(ts, dt, pos, sr): posizione vol-targeted sullo spread (decisa <= close[i]) + ritorno spread.""" pos = np.nan_to_num(f_statarb_resid(W=W_FROZEN, sgn=SGN_FROZEN)(j)) return (j["timestamp"].values.astype("int64"), pd.to_datetime(j["datetime"]).values, pos, spread_ret(j)) def _state_io(write: dict | None = None): if write is not None: STATE_DIR.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(write, indent=2)) return write return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None def _append(path: Path, rec: dict): STATE_DIR.mkdir(parents=True, exist_ok=True) with open(path, "a") as f: f.write(json.dumps(rec) + "\n") def init_state(j: pd.DataFrame) -> dict: ts, _, pos, _ = _signal(j) last = int(ts[-1]) return dict(start_ts=last, last_ts=last, n_bars=0, pos_modeled=float(pos[-1]), pos_real=float(pos[-1]), cap_modeled=MODELED_CAPITAL, cap_real=REAL_CAPITAL, peak_modeled=MODELED_CAPITAL, peak_real=REAL_CAPITAL, dd_modeled=0.0, dd_real=0.0, n_trades=0) def advance(st: dict, j: pd.DataFrame) -> dict: ts, dt, pos, sr = _signal(j) new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]] if not new: return st pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente) cm, cr = st["cap_modeled"], st["cap_real"] pkm, pkr, ddm, ddr = st["peak_modeled"], st["peak_real"], st["dd_modeled"], st["dd_real"] ntr = st.get("n_trades", 0) for i in new: s = float(sr[i]) tgt = float(pos[i]) # bersaglio deciso a close[i] (eseguito da i+1) # MODELED: rendimento dello spread tenuto durante la barra i, poi ribilancia al target net_m = pm * s - FEE_SIDE * 2.0 * abs(tgt - pm) # REAL-$600: salta i ribilanci sub-min_order (nozionale per-gamba |Δpos|*cap < $5) executed = abs(tgt - pr) * REAL_CAPITAL >= MIN_ORDER new_pr = tgt if executed else pr net_r = pr * s - FEE_SIDE * 2.0 * abs(new_pr - pr) if np.sign(new_pr) != np.sign(pr): _append(TRADES_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])), action="FLIP", from_pos=round(pr, 4), to_pos=round(new_pr, 4))) ntr += 1 cm *= (1.0 + max(net_m, -0.99)); cr *= (1.0 + max(net_r, -0.99)) pkm = max(pkm, cm); pkr = max(pkr, cr) ddm = max(ddm, (pkm - cm) / pkm if pkm > 0 else 0.0) ddr = max(ddr, (pkr - cr) / pkr if pkr > 0 else 0.0) pm, pr = tgt, new_pr _append(RETURNS_FILE, dict(ts=int(ts[i]), dt=str(pd.Timestamp(dt[i])), net_modeled=round(net_m, 6), net_real=round(net_r, 6), pos=round(new_pr, 4), cap_modeled=round(cm, 2), cap_real=round(cr, 2))) st.update(last_ts=int(ts[new[-1]]), n_bars=st.get("n_bars", 0) + len(new), pos_modeled=pm, pos_real=pr, cap_modeled=cm, cap_real=cr, peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_trades=ntr) return st def print_status(st: dict, j: pd.DataFrame): last_data = int(j["timestamp"].iloc[-1]) days = (last_data - st["start_ts"]) / 86400_000 rm = st["cap_modeled"] / MODELED_CAPITAL - 1 rr = st["cap_real"] / REAL_CAPITAL - 1 print("\n STATARB-RESID forward-monitor (PAPER — relative-value ETH/BTC, ortogonale, NON deploy)") print(f" config CONGELATA: W={W_FROZEN} sgn={SGN_FROZEN:+d} (residual relative-momentum, dollar-neutral)") print(f" forward da {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} " f"({st['n_bars']} barre 1d ~{days:.0f}g) flip di segno: {st['n_trades']}") print(f" posizione spread corrente (long ETH / short BTC per unità): {st['pos_real']:+.3f}") print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%") print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%") print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (atteso ~0: turnover basso a 1d)") print(f" log: {RETURNS_FILE}\n") def main(): ap = argparse.ArgumentParser() ap.add_argument("--status", action="store_true") ap.add_argument("--reset", action="store_true") args = ap.parse_args() j = build_joint("1d") if args.reset: for p in (STATE_FILE, TRADES_FILE, RETURNS_FILE): if p.exists(): p.unlink() st = init_state(j); _state_io(st) print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).") print_status(st, j); return st = _state_io() if st is None: st = init_state(j); _state_io(st) print("forward-monitor STATARB-RESID inizializzato (forward-only da ora).") print_status(st, j); return if not args.status: st = advance(st, j); _state_io(st) print_status(st, j) if __name__ == "__main__": main()