Files
PythagorasGoal/scripts/live/paper_statarb.py
T
Adriano Dal Pastro d597fc64e8 monitor: advance() consuma solo barre CHIUSE — serie rigenerate, guardia PREMATURO, 4 debiti chiusi
S5.1 RIPARATO E RIGENERATO. Filtro condiviso src/live/paper_guard.py (barra
open-labeled chiusa = ts + cadenza <= adesso) importato da tutti e 6 i monitor;
serie rigenerate dallo stesso start_ts con scripts/live/paper_regen.py (evidenza
in *.pre_regen_20260826.*): statarb +1,95 -> -1,61 (il ribaltamento del gate
27/09 previsto dall'audit), dvolspread -14,73 -> -4,41, xsr -4,98 -> -2,72,
prevday invariato. Nessuna data di gate si sposta. Guardia cablata in
monitor_health: stato PREMATURO (ultima barra che chiude dopo l'mtime, grazia
5 min, open_labeled=False per collect_chain) — sul dato vivo segnala i 5 rotti
e tace sui 2 sani; dopo la rigenerazione 7/7 OK. paper_portfolio non rigenerato
(GTAA su ADJUSTED_LAST: replay != serie registrata, P12), tolta la coda non
chiusa. D6 pagata di nuovo nel fix: asi8 in pandas 3 e' in us, non ns —
blindata con test su tre risoluzioni.

S5.12 ESTESO: conftest devia anche trades.db (wrapper su connect: il default
e' catturato alla definizione) e docs/journal/; book_executions.jsonl
sorvegliato con impronta inizio/fine suite.

S5.5 FATTO: test_leva_massima cancellato con nota (misurava frac*n_asset:
con una chiave di scala avrebbe continuato a passare smettendo di controllare).

S5.9 INDAGATO E RIPARATO (r0826_skh_band_drift): il dato regge (taglio 02/07
riproduce l'audit 1,6376, in-sample identico su ogni taglio); la deriva era la
finestra hold-out — e la sola settimana 15-22/08 vale +0,35 di Sharpe hold-out.
Il test ora taglia il feed al 02/07 e verifica la riproduzione stretta.

Suite: 751 passati, 0 falliti.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
2026-08-26 10:12:23 +00:00

180 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
from src.live import paper_guard as PG # 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)
# SOLO barre CHIUSE: `build_joint('1d')` passa da `resample_tf`, che NON scarta il giorno
# in corso (1 barra 1h su 24) — consumarlo faceva registrare ~4 min/giorno (r0822_monitor_audit)
new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D)
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()