feat(forward): cabla STATARB-RESID nel forward-monitor PAPER (lead ortogonale ETH/BTC)
Forward-monitor del LEAD dello sweep 2026-06-29 (relative-value ETH/BTC, dollar-neutral 2 gambe), il primo stream insieme ORTOGONALE (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600. - scripts/live/paper_statarb.py: forward-only, doppio libro MODELED($2000)/REAL-$600 (haircut fill), riusa il segnale ESATTO di orthogonal_signals.py (niente reimplementazione). Config CONGELATA W=45 sgn=+1. - Cablato in scripts/cron_daily.sh accanto a paper_prevday. Stato runtime in data/paper_statarb/ (gitignored). - test tests/test_paper_statarb.py (frozen config + advance forward/idempotente + haircut $600 basso). Correzione di etichetta (verificata): la cella vincente e' sgn=+1 -> NON mean-reversion ma relative-MOMENTUM sul residuo (dislocazioni ETH-vs-BTC continuano a 1d; sgn=-1 perde -1.4 IS). Diario + CLAUDE.md aggiornati. Test 146/146. Nessun deploy, forward-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ mkdir -p logs
|
||||
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
|
||||
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
|
||||
uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy)
|
||||
uv run python scripts/live/paper_statarb.py # forward-monitor lead STATARB-RESID ETH/BTC ortogonale (PAPER, non deploy)
|
||||
# NB: l'esecuzione Deribit e' passata al BOOK (TP01+SKH01 nettati) via scripts/cron_book.sh a
|
||||
# cadenza ORARIA (SKH01 e' a 230m: il daily mancherebbe gli ingressi). live_execute.py
|
||||
# (TP01-only) NON va piu' eseguito qui, sennò i due farebbero a pugni sullo stesso strumento.
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user