d597fc64e8
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
307 lines
15 KiB
Python
307 lines
15 KiB
Python
"""FORWARD-MONITOR — DVOLSPREAD (implied-vol relative-value BTC/ETH, 2 gambe), PAPER.
|
|
|
|
NON e' esecuzione reale. E' il monitoraggio forward-only del lead riabilitato il 2026-07-26
|
|
(diario `2026-07-26-wave-esecuzione-dvolspread-xsr-equity.md`, filone T2).
|
|
|
|
DA DOVE VIENE. `agent_14_dvol_spread` (onda ortho, 2026-06-21) fu **l'unico sopravvissuto** al
|
|
marginal scorer indurito: dei 18 book relative-value che facevano ADDS con lo scorer vecchio,
|
|
l'indurito ne lascio' in piedi uno solo, questo. Poi resto' in limbo **35 giorni** — non nel book,
|
|
non in un monitor, non rifiutato. Il T2 del 26/07 l'ha ripreso e passato ai due gate che nel giugno
|
|
non esistevano (`deflated_sharpe`, `select_cell_insample`): li supera, ma solo con i parametri
|
|
scelti ONESTAMENTE in-sample, e con un margine sottile. Questo file esiste perche' la lezione (c)
|
|
di quell'ondata e' **"un lead in forward-monitor senza monitor e senza scadenza e' un lead perso"**.
|
|
|
|
COS'E'. Segnale = `log(DVOL_btc) - log(DVOL_eth)`: quando il mercato delle opzioni paga di piu' la
|
|
vol implicita di una gamba (la gamba "temuta"), ci si posiziona **verso** quella gamba — tesi
|
|
volatility-risk-premium / fear-reversal: la gamba le cui opzioni sono comprate per paura e' quella
|
|
premiata quando il movimento temuto non si materializza. Market-neutral per costruzione
|
|
(w_eth == -w_btc) -> corr a TP01 ~0.11, ed **eseguibile a $600** (2 gambe, cap $300/asset).
|
|
|
|
CONFIG CONGELATA — i parametri ONESTI, non quelli pubblicati (ogni ritocco AZZERA la finestra):
|
|
zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60
|
|
La cella pubblicata dall'agente (zwin=150, k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50) era
|
|
**83a/729 sull'hold-out ma 471a/729 in-sample** = selection-on-holdout, e **fallisce** il
|
|
deflated-Sharpe (0.947). La cella qui congelata e' scelta col solo in-sample e lo **passa (0.953)**.
|
|
⚠️ Il numero da citare per questo lead e' **hold-out 0.69**, NON lo 0.93 che circolava.
|
|
Riusa la fabbrica ESATTA di `scripts/research/r0726_dvolspread_gate.make_book` (nessuna
|
|
reimplementazione -> niente drift fra monitor e backtest).
|
|
|
|
PERCHE' MONITOR E NON BOOK (i tre motivi, dichiarati):
|
|
1. **campione ATTIVO 1949 giorni su 2691 (72%)**: prima del 2021-03 il DVOL non esiste e il book
|
|
e' correttamente flat -> l'**hold-out attivo e' solo 1.6 anni**;
|
|
2. **margine deflated-Sharpe sul filo** (0.953 vs soglia 0.95) su 729 trial: un passaggio, non
|
|
un'assoluzione;
|
|
3. **`weights_tilt_null` mai affrontato** — ogni proposta di cambio pesi deve superarlo.
|
|
|
|
STRUMENTAZIONE SPECIFICA DI QUESTO LEAD (la ragione per cui non basta copiare paper_statarb):
|
|
il book va **flat quando il DVOL manca**. Un feed DVOL rotto produrrebbe giorni piatti che, contati
|
|
come evidenza, direbbero "nessuna perdita" invece di "nessun dato". Il monitor quindi separa
|
|
* giorni ATTIVI (DVOL presente, posizione != 0)
|
|
* giorni FLAT-DA-SEGNALE (DVOL presente, il segnale sceglie 0)
|
|
* giorni FLAT-SENZA-DATO (DVOL assente/stantio -> NON contano come finestra forward)
|
|
e riporta l'eta' dell'ultimo DVOL. La finestra forward si misura in **barre attive**, non in giorni
|
|
di calendario.
|
|
|
|
GATE PRE-REGISTRATO (scritto PRIMA di vedere la finestra forward — 2026-07-26):
|
|
* **kill anticipato 2026-10-24 (~90g):** se lo Sharpe forward MODELED < -0.50 -> RITIRO.
|
|
* **decisione 2027-01-24 (~180g):** promozione a candidato di book SOLO se, sul campione esteso,
|
|
tutte e quattro:
|
|
(a) Sharpe forward > 0 [barra NECESSARIA, non sufficiente: con ~180
|
|
barre attive SE(Sharpe) ~ 1.4, quindi una
|
|
soglia piu' alta sarebbe finta precisione]
|
|
(b) marginale vs TP01 ancora ADDS + robust_oos + has_insample_edge
|
|
(c) deflated-Sharpe ricalcolato sul campione esteso >= 0.95 [se il margine 0.953 non
|
|
MIGLIORA con piu' dati,
|
|
l'edge non c'e']
|
|
(d) `weights_tilt_null` superato al peso proposto
|
|
* **veto d'integrita':** se le barre ATTIVE sono < 80% della finestra, la finestra NON conta ->
|
|
si estende, non si decide su dati mancanti.
|
|
|
|
DUE LIBRI (onesta' sull'esecuzione):
|
|
MODELED $2000 : ribilanciamento continuo di entrambe le gambe.
|
|
REAL $600 : min-order $5 per gamba -> salta i ribilanci sub-min-order. Il gap fra i due
|
|
E' l'haircut di fill reale.
|
|
|
|
Stato: data/paper_dvolspread/{state.json, returns.jsonl} (append-only).
|
|
|
|
uv run python scripts/live/paper_dvolspread.py # avanza col dato disponibile
|
|
uv run python scripts/live/paper_dvolspread.py --status # solo stato, non avanza
|
|
uv run python scripts/live/paper_dvolspread.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"))
|
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
|
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "ortho"))
|
|
|
|
import altlib as al # noqa: E402
|
|
from src.live import paper_guard as PG # noqa: E402
|
|
import ortholib as ol # noqa: E402
|
|
from r0726_dvolspread_gate import make_book # noqa: E402 (fabbrica ESATTA del T2)
|
|
|
|
STATE_DIR = PROJECT_ROOT / "data" / "paper_dvolspread"
|
|
STATE_FILE = STATE_DIR / "state.json"
|
|
RETURNS_FILE = STATE_DIR / "returns.jsonl"
|
|
|
|
# --- CONFIG CONGELATA: la cella scelta IN-SAMPLE (non quella pubblicata) --------------------
|
|
FROZEN = dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60)
|
|
FEE_SIDE = 0.0005 # 0.05%/lato per gamba (Deribit taker)
|
|
MODELED_CAPITAL = 2000.0
|
|
REAL_CAPITAL = 600.0
|
|
MIN_ORDER = 5.0
|
|
CAP_NOTIONAL = 300.0 # guardrail live: $300 di nozionale per asset
|
|
|
|
# --- GATE PRE-REGISTRATO (2026-07-26) ------------------------------------------------------
|
|
KILL_DATE = "2026-10-24" # se Sharpe forward < -0.50 -> ritiro
|
|
KILL_SHARPE = -0.50
|
|
GATE_DATE = "2027-01-24" # decisione piena
|
|
MIN_ACTIVE_FRAC = 0.80 # veto d'integrita': sotto, la finestra non conta
|
|
DVOL_MAX_AGE_D = 3.0 # oltre, il DVOL e' stantio -> giorni non contati come attivi
|
|
|
|
|
|
def _panel():
|
|
"""BTC/ETH 1d allineati + posizioni del book congelato + eta' del DVOL per barra."""
|
|
btc, eth = ol.aligned()
|
|
wb, we = make_book(**FROZEN)(btc, eth)
|
|
rb = al.simple_returns(btc["close"].values.astype(float))
|
|
re = al.simple_returns(eth["close"].values.astype(float))
|
|
# il book e' flat dove il DVOL manca: lo si rileva dalla fonte, non dal peso (un peso 0 puo'
|
|
# essere una scelta del segnale in regime compatto, ed e' un'informazione DIVERSA)
|
|
db = al.dvol(btc, "BTC")
|
|
de = al.dvol(eth, "ETH")
|
|
has_dvol = np.isfinite(db) & np.isfinite(de)
|
|
return dict(ts=btc["timestamp"].values.astype("int64"),
|
|
dt=pd.to_datetime(btc["datetime"]).values,
|
|
wb=np.nan_to_num(np.asarray(wb, float)),
|
|
we=np.nan_to_num(np.asarray(we, float)),
|
|
rb=rb, re=re, has_dvol=has_dvol)
|
|
|
|
|
|
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(P: dict) -> dict:
|
|
last = int(P["ts"][-1])
|
|
return dict(start_ts=last, last_ts=last, n_bars=0, n_active=0,
|
|
n_flat_signal=0, n_flat_nodata=0,
|
|
wb_modeled=float(P["wb"][-1]), wb_real=float(P["wb"][-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_flips=0, frozen=FROZEN)
|
|
|
|
|
|
def advance(st: dict, P: dict) -> dict:
|
|
ts, dt = P["ts"], P["dt"]
|
|
# SOLO barre CHIUSE: il pannello arriva da `resample_tf` col giorno in corso dentro —
|
|
# su uno spread BTC-ETH la barra troncata e' quasi solo rumore (~2 min/g, r0822_monitor_audit)
|
|
new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D)
|
|
if not new:
|
|
return st
|
|
hm, hr = st["wb_modeled"], st["wb_real"] # peso BTC TENUTO (w_eth = -w_btc)
|
|
cm, cr = st["cap_modeled"], st["cap_real"]
|
|
pkm, pkr = st["peak_modeled"], st["peak_real"]
|
|
ddm, ddr = st["dd_modeled"], st["dd_real"]
|
|
nfl, nac, nfs, nfn = (st["n_flips"], st["n_active"],
|
|
st["n_flat_signal"], st["n_flat_nodata"])
|
|
for i in new:
|
|
# rendimento del book TENUTO durante la barra i (spread: long BTC / short ETH per unita')
|
|
s = float(P["rb"][i] - P["re"][i])
|
|
tgt = float(P["wb"][i]) # bersaglio deciso a close[i], eseguito da i+1
|
|
# MODELED: ribilanciamento continuo, fee su ENTRAMBE le gambe
|
|
net_m = hm * s - FEE_SIDE * 2.0 * abs(tgt - hm)
|
|
# REAL $600: salta i ribilanci sotto il min-order per gamba
|
|
executed = abs(tgt - hr) * REAL_CAPITAL >= MIN_ORDER
|
|
new_hr = tgt if executed else hr
|
|
net_r = hr * s - FEE_SIDE * 2.0 * abs(new_hr - hr)
|
|
# contabilita' dell'ATTIVITA' — la distinzione che rende il monitor leggibile
|
|
if not bool(P["has_dvol"][i]):
|
|
nfn += 1
|
|
elif abs(tgt) < 1e-12:
|
|
nfs += 1
|
|
else:
|
|
nac += 1
|
|
if np.sign(new_hr) != np.sign(hr) and abs(new_hr) > 1e-12:
|
|
nfl += 1
|
|
cm *= (1.0 + max(net_m, -0.99))
|
|
cr *= (1.0 + max(net_r, -0.99))
|
|
pkm, pkr = max(pkm, cm), 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)
|
|
hm, hr = tgt, new_hr
|
|
_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),
|
|
w_btc=round(new_hr, 4), has_dvol=bool(P["has_dvol"][i]),
|
|
cap_modeled=round(cm, 2), cap_real=round(cr, 2)))
|
|
st.update(last_ts=int(ts[new[-1]]), n_bars=st["n_bars"] + len(new),
|
|
n_active=nac, n_flat_signal=nfs, n_flat_nodata=nfn,
|
|
wb_modeled=hm, wb_real=hr, cap_modeled=cm, cap_real=cr,
|
|
peak_modeled=pkm, peak_real=pkr, dd_modeled=ddm, dd_real=ddr, n_flips=nfl)
|
|
return st
|
|
|
|
|
|
def _forward_sharpe(key: str = "net_modeled") -> float:
|
|
"""Sharpe della sola finestra forward, sulle barre registrate (annualizzato a 365)."""
|
|
if not RETURNS_FILE.exists():
|
|
return float("nan")
|
|
v = []
|
|
with open(RETURNS_FILE) as f:
|
|
for line in f:
|
|
try:
|
|
v.append(float(json.loads(line)[key]))
|
|
except (ValueError, KeyError):
|
|
continue
|
|
a = np.asarray(v, float)
|
|
a = a[np.isfinite(a)]
|
|
return float(a.mean() / a.std() * np.sqrt(365.25)) if len(a) > 30 and a.std() > 0 else float("nan")
|
|
|
|
|
|
def print_status(st: dict, P: dict) -> None:
|
|
now = int(P["ts"][-1])
|
|
days = (now - st["start_ts"]) / 86400_000
|
|
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
|
|
rr = st["cap_real"] / REAL_CAPITAL - 1
|
|
counted = st["n_active"] + st["n_flat_signal"]
|
|
frac = counted / st["n_bars"] if st["n_bars"] else 1.0
|
|
dvol_ok = bool(P["has_dvol"][-1])
|
|
sh = _forward_sharpe()
|
|
|
|
print("\n DVOLSPREAD forward-monitor (PAPER — implied-vol relative-value BTC/ETH, NON deploy)")
|
|
print(f" config CONGELATA (cella scelta IN-SAMPLE): {FROZEN}")
|
|
print(" ⚠ numero da citare per questo lead: hold-out 0.69 (NON lo 0.93 della cella pubblicata)")
|
|
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_flips']}")
|
|
print(f" attivita': ATTIVE {st['n_active']} | flat-da-segnale {st['n_flat_signal']} | "
|
|
f"FLAT-SENZA-DATO {st['n_flat_nodata']} -> valide {frac:.0%} "
|
|
f"(veto sotto {MIN_ACTIVE_FRAC:.0%})")
|
|
print(f" DVOL all'ultima barra: {'presente' if dvol_ok else '*** ASSENTE ***'}")
|
|
print(f" posizione corrente (w_BTC, w_ETH = -w_BTC): {st['wb_real']:+.3f} "
|
|
f"-> nozionale ${abs(st['wb_real']) * REAL_CAPITAL:,.0f}/gamba "
|
|
f"(cap ${CAP_NOTIONAL:,.0f})")
|
|
print(f" MODELED ($2000): {rm * 100:+6.2f}% eq ${st['cap_modeled']:.2f} "
|
|
f"maxDD {st['dd_modeled'] * 100:.1f}%")
|
|
print(f" REAL-$600 : {rr * 100:+6.2f}% eq ${st['cap_real']:.2f} "
|
|
f"maxDD {st['dd_real'] * 100:.1f}%")
|
|
print(f" -> fill-haircut MODELED-REAL: {(rm - rr) * 100:+.2f} pp")
|
|
if np.isfinite(sh):
|
|
print(f" Sharpe forward (MODELED, {st['n_bars']} barre): {sh:+.2f}")
|
|
else:
|
|
print(f" Sharpe forward: n/d (servono >30 barre, ora {st['n_bars']})")
|
|
|
|
today = pd.Timestamp(now, unit="ms", tz="UTC").normalize()
|
|
print(f"\n GATE PRE-REGISTRATO (26/07)")
|
|
if today >= pd.Timestamp(KILL_DATE, tz="UTC"):
|
|
if np.isfinite(sh) and sh < KILL_SHARPE:
|
|
print(f" *** KILL {KILL_DATE}: Sharpe {sh:+.2f} < {KILL_SHARPE:+.2f} -> RITIRARE ***")
|
|
else:
|
|
print(f" kill-check {KILL_DATE}: superato (Sharpe {sh:+.2f} >= {KILL_SHARPE:+.2f})")
|
|
else:
|
|
print(f" kill anticipato il {KILL_DATE} se Sharpe < {KILL_SHARPE:+.2f} "
|
|
f"(fra {(pd.Timestamp(KILL_DATE, tz='UTC') - today).days}g)")
|
|
if today >= pd.Timestamp(GATE_DATE, tz="UTC"):
|
|
if frac < MIN_ACTIVE_FRAC:
|
|
print(f" *** VETO D'INTEGRITA': solo {frac:.0%} di barre valide -> ESTENDERE ***")
|
|
else:
|
|
print(f" *** DECISIONE DOVUTA ({GATE_DATE}): valutare (a) Sharpe>0 [{sh:+.2f}] "
|
|
f"(b) marginale ADDS (c) deflated-Sharpe>=0.95 (d) weights_tilt_null ***")
|
|
else:
|
|
print(f" decisione piena il {GATE_DATE} "
|
|
f"(fra {(pd.Timestamp(GATE_DATE, tz='UTC') - today).days}g): Sharpe>0 E marginale ADDS "
|
|
f"E deflated-Sharpe>=0.95 E weights_tilt_null")
|
|
print(f" log: {RETURNS_FILE}\n")
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--status", action="store_true")
|
|
ap.add_argument("--reset", action="store_true")
|
|
args = ap.parse_args()
|
|
P = _panel()
|
|
if args.reset:
|
|
for p in (STATE_FILE, RETURNS_FILE):
|
|
if p.exists():
|
|
p.unlink()
|
|
st = _state_io(init_state(P))
|
|
print("forward-monitor DVOLSPREAD inizializzato (forward-only da ora).")
|
|
print_status(st, P)
|
|
return
|
|
st = _state_io()
|
|
if st is None:
|
|
st = _state_io(init_state(P))
|
|
print("forward-monitor DVOLSPREAD inizializzato (forward-only da ora).")
|
|
print_status(st, P)
|
|
return
|
|
if st.get("frozen") != FROZEN:
|
|
print("*** CONFIG CAMBIATA rispetto allo stato salvato: la finestra forward non e' piu'")
|
|
print(" valida. Rilanciare con --reset (e dichiararlo nel diario).")
|
|
print(f" salvata: {st.get('frozen')}\n attuale: {FROZEN}")
|
|
sys.exit(1)
|
|
if not args.status:
|
|
st = advance(st, P)
|
|
_state_io(st)
|
|
print_status(st, P)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|