diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 356cf84..4263a1b 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -696,6 +696,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 28 | DEPEG | il falsificatore che l'autore di 21 ha nominato contro se stesso: il depeg USDT contamina il ranking? | **SOPRAVVIVE** — il venue sposta 0,01 di Sharpe | | 29 | PROP-RECAL | J rifatto con XS01 dalla storia lunga: il canale funded si apre? | **LEAD ridimensionato ~5x** — 42% → **7,8%** | | 30 | LISTINO | `GATE PROP-01` gamba (a): la firm lista >=10 delle 13 gambe con short? | **PASS 13/13** — verificato sul venue, non sul sito | +| 31 | GATE-RECON | cosa direbbero i 3 gate pre-registrati se le serie forward fossero riparate | **1 gate su 3 ribaltato** — e la premessa e' CONFERMATA | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | | 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** — filone chiuso definitivamente | | 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | @@ -1361,3 +1362,72 @@ prezzo esatto del taglio $100k sono **primari-dichiarativi + secondari concordi* 📌 **Conseguenza sul gate:** la gamba (a) e' **CHIUSA con un PASS**. Restano la **(b)** — banda d'ancora di SKH01 sotto la lente prop, che un agente sta girando ora invece di aspettare il 31/10 — e la **(c)**, che va **ricalcolata** con la quota come deposito rimborsabile invece che come spesa. + +--- + +## 31 — GATE-RECON (cosa direbbero i gate con le serie riparate) — **sola lettura, verificata** + +`scripts/research/r0822c_gate_recon.py` (719 righe, ~25 s). **Zero celle di selezione**: e' un audit, +tutte le soglie **lette dai file pre-registrati**, nessun parametro scelto dall'agente. +✅ **Sola lettura provata**: md5 + mtime di **ogni** file `data/paper_*` invariati dopo la corsa. + +| gate | serie | n | Sharpe | maxDD | vol ann. | +|---|---|---|---|---|---| +| **STATARB** | **attuale** | 54 | **+1,96** | **2,24%** | 7,88% | +| **STATARB** | **riparata** | 53 | **−2,02** | **15,19%** | **31,42%** | +| XSR01 | attuale | 28 | −6,55 | 0,28% | 0,46% | +| XSR01 | riparata | 27 | −0,99 | 0,82% | 2,78% | +| DVOLSPREAD | attuale | 28 | −15,82 | 0,93% | 0,79% | +| DVOLSPREAD | riparata | 27 | −5,21 | 7,11% | 19,36% | + +Minuti di mercato registrati al giorno: **3,9 / 41,2 / 2,5** = ✅ replica indipendente dei 4/41/2 +pubblicati stamattina. + +**`STATARB 27/09 — riparata: RITIRO · attuale: CANDIDATO AL DEPLOY`** +**`XSR01 23/10 — riparata: RITIRO · attuale: RITIRO`** +**`DVOLSPREAD 24/10 — riparata: KILL · attuale: KILL`** +*(Non sono i verdetti dei gate: sono i criteri letti **oggi**, su finestre incomplete — 54/90, 28/90, +28/91 barre. Anticipare un gate pre-registrato e' selezione sull'hold-out, e l'agente lo dichiara.)* + +✅ **LA PREMESSA SU CUI POGGIA TUTTO E' ORA CONFERMATA PER VIA DIRETTA, non piu' di traverso:** +`data/_feed_backup/*.prebuild.bak` conserva il **vintage pre-riscrittura** di stanotte → +**0 barre chiuse cambiate su 1.761.259** (BTC/ETH 1h+5m). L'unica riga cambiata per file e' **l'ultima +del vintage, che era aperta** (BTC 21/08: close 73732,5→73690, volume 342→492 = **il difetto stesso, +in chiaro**). Su 62 cicli `paper_prevday` fa 1427/1488 identiche, con tasso di divergenza **piatto +nell'eta'** (2,2-5,1%, nessuna crescita) e concentrato all'ora del cron. +⚠️ **Limite dichiarato:** `hl_*_1d` e `dvol_*` **non hanno vintage su disco** → per XSR01 e +DVOLSPREAD l'evidenza resta **indiretta**. + +**Prove d'identita' obbligatorie prima di ogni confronto:** **I1** pannello letto ora == quello del +cron di stanotte (`max|d| = 0`, 3/3, contro lo `state.json` scritto alle 00:35); **I2** barre gia' +chiuse identiche al bit (statarb 6/54, xsr 1/28, **dvolspread 0/28 → per quel monitor la prova I2 NON +ESISTE, dichiarato**); **I3** troncare l'INPUT == affettare l'output (`max|d| = 0`, 3/3). + +🚨 **(1) IL VETO D'INTEGRITA' DI DVOLSPREAD NON SCATTA: 28 attive / 28 = 100% in ENTRAMBE le lenti.** +Una serie che misura **2 minuti di mercato al giorno** passa al **100%** un veto progettato apposta per +non far decidere su dati mancanti. **Non e' un problema di taratura:** il veto conta *se il DVOL +c'era*, non *quanto dura la barra su cui c'era* — **assi ortogonali**. La stessa soglia 0,80 applicata +alla quota di barre **chiuse** leggerebbe **0%** e vieterebbe. **Variante nuova di una regola vecchia: +una barra presente non e' una giornata presente.** +🚨 **(2) Il caso peggiore NON e' quello coi numeri piu' assurdi.** XSR01 (|Sh| ×6,6) e DVOLSPREAD +(×3,0) dicono **lo stesso verdetto** in entrambe le lenti; **STATARB ha |Sharpe| quasi identico +(1,96 contro 2,02) e cambia SEGNO** — ed e' il monitor del gate **piu' vicino**. Un controllo che +cercasse *"numeri troppo belli"* **lo lascerebbe passare**. 📌 **La firma da cercare non e' il +|Sharpe| ma la vol forward molto sotto quella del backtest** (7,88% contro 31,42%). +📌 **(3) L'asimmetria del 25/07 su STATARB:** la diagnostica pre-registrata (statica sempre-short) +**non e' affetta** — si ricalcola dal **pannello**, non dalla serie registrata (varia di −0,01 fra le +due finestre contro −3,97 della strategia). **Al gate si confronterebbe un benchmark GIUSTO con un +numero di strategia SBAGLIATO**, e il margine sembrerebbe **+5,83 invece di +1,87**. Il segno del +margine sopravvive: il difetto ribalta il gate, non questa diagnostica. + +📌 **QUANTO COSTA ASPETTARE — l'unico argomento quantitativo su *quando* riparare.** Con +**rigenerazione**: **zero barre perse a qualunque data**; si paga solo il rischio che la riparazione +non arrivi. **Senza**: riparare **oggi** lascia **36/90** barre vere a STATARB (62/90 a XSR01, 63/91 a +DVOLSPREAD); **alla vigilia, 1**. E la riparazione parziale **non contamina la finestra, la accorcia** +— una barra troncata porta ~1/370 della varianza *e* del rendimento di una vera, quindi non pesa: +riparando oggi, il 27/09 si leggerebbe una finestra **piu' corta ma VERA** (SE(Sharpe) ~3,2 invece di +~2,0). **Riparare alla vigilia no.** + +⚠️ `implausible_sharpe` **girato per la prima volta sulle serie forward**: chiede n≥30, quindi **2 +serie su 3 non sono giudicabili**, e su statarb dice "ok" **in entrambe le lenti** — **il gate non +vede un segno ribaltato**. diff --git a/scripts/research/r0822c_gate_recon.py b/scripts/research/r0822c_gate_recon.py new file mode 100644 index 0000000..3e399c7 --- /dev/null +++ b/scripts/research/r0822c_gate_recon.py @@ -0,0 +1,719 @@ +"""GATE-RECON — cosa direbbero i tre gate pre-registrati se le serie forward fossero giuste. + +IL FATTO DA CUI NASCE (filone MONITOR-AUDIT, 22/08, gia' misurato e NON riparato per scelta). +`fetch_hyperliquid` e `resample_tf` scrivono la barra del giorno IN CORSO; `advance()` la consuma +e porta `last_ts` su di essa -> le ore restanti non entrano in nessun rendimento. Quattro monitor +su sei registrano una FRAZIONE del giorno (2, 4, 19 e 41 minuti su 1440). Tre decisioni +pre-registrate leggono serie costruite cosi': + STATARB 2026-09-27 r0724_statarb_deploy_gate (Sh>=0.5, maxDD<10%, haircut<0.5pp) + XSR01 2026-10-23 r0725_xsr_deploy_gate (Sh>=1.0 E haircut$5000<=40%) + DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread) + +QUESTO SCRIPT NON RIPARA E NON DECIDE. La riparazione e' una decisione dell'operatore e i gate +hanno una data: anticiparli sarebbe selezione-sul-forward, il bias che questo progetto ha gia' +pagato due volte. La consegna e' un DELTA, non un verdetto: + "alla data di oggi, con la serie riparata il criterio direbbe Y; con la serie com'e', Z" +e la distanza fra Y e Z e' la misura del danno del difetto — l'unica cosa che serve per decidere +SE e QUANDO riparare. Sola lettura su tutto cio' che e' produzione: i moduli di `scripts/live/` +si importano senza chiamarne `main()`, e `_append` viene sostituito con un raccoglitore in memoria +PRIMA di qualunque chiamata ad `advance()` -> nessun file di `data/` viene aperto in scrittura. + +METODO. La serie RIPARATA non e' una reimplementazione: e' il codice di produzione rieseguito +dall'inception sui dati di oggi, TOLTA l'ultima barra (l'unica ancora aperta). Tutto il resto del +pannello e' fatto di barre chiuse, quindi il replay E' la serie che il monitor avrebbe registrato +se avesse aspettato la chiusura. Questo poggia su due condizioni, entrambe verificate qui: + (P1) il feed non riscrive le barre gia' chiuse -> sezione 0; + (P2) il pannello che leggo ora e' quello che il cron ha letto stanotte, e troncare l'ULTIMA + barra non cambia nessuna barra precedente -> sezione 1 (tre prove d'identita'). +Senza (P2) starei ricostruendo una strategia diversa invece della stessa strategia riparata, ed e' +il modo piu' facile di sbagliare questo lavoro. + +COSA MI ASPETTAVO PRIMA DI MISURARE (registrato qui perche' sia falsificabile): + * la premessa (P1) vera ma verificabile solo di traverso -> **sbagliato, ed e' la sorpresa + utile**: `data/_feed_backup/*.prebuild.bak` conserva il vintage PRE-riscrittura di stanotte, + quindi la premessa si verifica DIRETTAMENTE su ~914.000 barre, senza passare da una strategia; + * i tre gate tutti ribaltati -> **sbagliato, e la ragione conta**: solo STATARB si ribalta. + XSR01 e DVOLSPREAD dicono "ritiro/kill" su ENTRAMBE le lenti, ma con numeri che differiscono + di 3-7x e che a 28 barre non sono letture di gate su nessuna delle due; + * il veto d'integrita' di DVOLSPREAD (l'unico dei tre che ha una guardia) capace di accorgersene + -> **confermato che NON se ne accorge**, e per una ragione strutturale: conta se il DVOL C'ERA, + non quanto dura la barra su cui c'era. + + nice -n 19 timeout 900 uv run python scripts/research/r0822c_gate_recon.py +""" +from __future__ import annotations + +import importlib +import importlib.util +import json +import sys +from datetime import date +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +BAK = ROOT / "data" / "_feed_backup" +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", + ROOT / "scripts" / "research" / "ortho"): + sys.path.insert(0, str(_p)) + +ANN = np.sqrt(365.0) +TODAY = date.today() +H = "=" * 100 +# Incidente del feed 2026-07-09 -> 2026-07-15 (`rebuild_history` abortito 7 giri, EPERM sul backup, +# gia' riparato): col feed fermo i monitor non trovavano barre nuove e al ripristino ne hanno +# recuperate in blocco, GIA' CHIUSE. Sono le uniche barre corrette di `paper_statarb` e vanno +# escluse da ogni stima del difetto, o gli si attribuisce un'attenuazione che viene da altro. +FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC")) + + +# -------------------------------------------------------------------------------------------- +# infrastruttura di sola lettura (stessa di r0822_monitor_audit: nessuna reimplementazione) +# -------------------------------------------------------------------------------------------- +_MODS: dict[str, object] = {} + + +def live(name: str): + """Esegue un modulo di `scripts/live/` per averne le DEFINIZIONI. Mai `main()`.""" + if name in _MODS: + return _MODS[name] + spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + _MODS[name] = m + return m + + +class Sink: + """Sostituisce `_append`: raccoglie in memoria invece di scrivere. E' l'unica ragione per cui + si puo' rieseguire il codice di produzione TALE E QUALE restando in sola lettura.""" + + def __init__(self): + self.rows: list[tuple[str, dict]] = [] + + def __call__(self, path, rec): + self.rows.append((Path(path).name, dict(rec))) + + def of(self, fname: str) -> list[dict]: + return [r for n, r in self.rows if n == fname] + + +def read_jsonl(p: Path) -> list[dict]: + return [json.loads(x) for x in p.read_text().splitlines() if x.strip()] if p.exists() else [] + + +def mets(a) -> dict: + """Sharpe (365), maxDD e ritorno cumulato di una serie di rendimenti netti per barra.""" + a = np.asarray(a, float) + a = a[np.isfinite(a)] + if len(a) < 2 or a.std() == 0: + return dict(n=len(a), sh=float("nan"), dd=float("nan"), tot=float("nan"), + mu=float("nan"), sd=float("nan")) + eq = np.cumprod(1.0 + a) + pk = np.maximum.accumulate(eq) + return dict(n=len(a), sh=float(a.mean() / a.std() * ANN), + dd=float(np.max((pk - eq) / pk)), tot=float(eq[-1] - 1.0), + mu=float(a.mean() * 365.0), sd=float(a.std() * ANN)) + + +def se_sharpe(n: int) -> float: + """Errore standard grezzo di uno Sharpe annualizzato su n barre giornaliere (~sqrt(365/n)).""" + return float(np.sqrt(365.0 / max(n, 1))) + + +# ============================================================================================ +# 0 — LA PREMESSA: rigenerare non perde la finestra SOLO se le barre chiuse non vengono riscritte +# ============================================================================================ +def sezione_0() -> dict: + print(H) + print(" 0 — PREMESSA: il feed riscrive le barre gia' CHIUSE?") + print(H) + print(""" + Perche' e' la premessa di tutto: la raccomandazione del filone MONITOR-AUDIT e' "riparare + `advance()` + RIGENERARE", cioe' ricalcolare la finestra forward dall'inception sui dati di + oggi. Se il feed riscrivesse le barre chiuse, quella rigenerazione produrrebbe una serie + DIVERSA da quella che si sarebbe registrata giorno per giorno -> la finestra andrebbe persa e + la conclusione cambierebbe. E' stata verificata due volte di traverso (via strategie); qui si + verifica una terza volta e in modo DIRETTO.""") + + out = {} + print("\n 0a. VERIFICA DIRETTA sul dato grezzo — vintage PRE-riscrittura contro oggi") + print(" `rebuild_history` copia il parquet in data/_feed_backup/*.prebuild.bak PRIMA di") + print(" sovrascriverlo: sul disco c'e' il file com'era ieri sera. Nessuna strategia in mezzo.") + tot_ch, tot_div = 0, 0 + for a in ("btc", "eth"): + for tf in ("1h", "5m"): + new_p, old_p = RAW / f"{a}_{tf}.parquet", BAK / f"{a}_{tf}.parquet.prebuild.bak" + if not (new_p.exists() and old_p.exists()): + print(f" {a}_{tf}: vintage assente -> NON MISURATO") + continue + new = pd.read_parquet(new_p) + old = pd.read_parquet(old_p) + m = old.merge(new, on="timestamp", suffixes=("_o", "_n")) + cols = ["open", "high", "low", "close", "volume"] + diff = np.zeros(len(m), bool) + for c in cols: + diff |= (m[f"{c}_o"].values != m[f"{c}_n"].values) + last_old = int(old["timestamp"].max()) + chiuse = m["timestamp"].values < last_old # nel vecchio file l'ultima riga era APERTA + nch, nd = int(chiuse.sum()), int((diff & chiuse).sum()) + tot_ch += nch + tot_div += nd + print(f" {a}_{tf}: {nch:,} barre gia' chiuse nel vintage -> DIVERSE OGGI: {nd}" + f" (righe cambiate in tutto: {int(diff.sum())})") + if diff.sum() == 1: + i = int(np.where(diff)[0][0]) + ts = pd.Timestamp(int(m["timestamp"].iloc[i]), unit="ms", tz="UTC") + print(f" l'unica riga cambiata e' {ts} = l'ULTIMA del vintage, che era " + f"aperta: close {m['close_o'].iloc[i]:g} -> {m['close_n'].iloc[i]:g}, " + f"volume {m['volume_o'].iloc[i]:,.0f} -> {m['volume_n'].iloc[i]:,.0f}") + out["chiuse"], out["div"] = tot_ch, tot_div + print(f"\n TOTALE: {tot_div} barre chiuse cambiate su {tot_ch:,} " + f"-> la riscrittura notturna e' un NO-OP sulle barre chiuse.") + print(" ⚠ questo e' UN ciclo di riscrittura (ieri->oggi). La stessa riga mostra anche il") + print(" DIFETTO: l'ultima riga del vintage era una barra parziale, ed e' esattamente") + print(" quella che i monitor consumano.") + + print("\n 0b. VERIFICA su 62 CICLI — tasso di divergenza per ETA' della barra (paper_prevday)") + print(" Statistica diversa da quella del 22/08 (che contava le identiche): se il feed") + print(" riscrivesse le barre chiuse, il tasso CRESCEREBBE con l'eta' (piu' riscritture") + print(" subite). Se il difetto e' solo la barra parziale al momento della registrazione,") + print(" il tasso e' PIATTO nell'eta' e vale ~1/24.") + pp = live("paper_prevday") + st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl") + dfs = pp.build_bars() + S = st["start_ts"] + pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS} + sink = Sink() + pp._append = sink + st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos), + cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL, + peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_trades=0) + pp.advance(st0, dfs) + rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")} + pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])]) + for r in rec if int(r["ts"]) in rep] + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True)) + A = np.array([p[1] for p in pairs]) + B = np.array([p[2] for p in pairs]) + dv = np.abs(A - B) > 1e-6 + now = pd.Timestamp.now(tz="UTC") + age = (now - T).days + d = pd.DataFrame(dict(age=age, div=dv)) + d["b"] = pd.cut(d.age, [-1, 7, 14, 21, 30, 45, 60, 999], + labels=["0-7g", "8-14g", "15-21g", "22-30g", "31-45g", "46-60g", ">60g"]) + g = d.groupby("b", observed=True).agg(n=("div", "size"), div=("div", "sum")) + g["tasso"] = g["div"] / g["n"] + print(f" {len(A)} barre orarie appaiate, {int(dv.sum())} divergenti " + f"({dv.mean() * 100:.2f}%; identiche {len(A) - int(dv.sum())}/{len(A)})") + print(" " + " ".join(f"{ix}:{r.tasso * 100:4.1f}%" for ix, r in g.iterrows())) + hh = pd.Series(T.hour[dv]).value_counts().sort_index() + print(" ora UTC delle divergenti: " + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in hh.items()) + + " <- il cron gira alle 00:30") + out["prevday_rate_flat"] = (float(g["tasso"].min()), float(g["tasso"].max())) + + print("\n 0c. LIMITE DICHIARATO — cosa NON e' verificato direttamente") + print(" Il vintage prebuild esiste solo per BTC/ETH (lo scrive `rebuild_history`).") + print(" `hl_*_1d.parquet` (che alimenta XSR01) e `dvol_*.parquet` NON hanno vintage su") + print(" disco: per loro l'evidenza resta indiretta (le barre che i monitor hanno letto") + print(" gia' chiuse coincidono al bit dopo settimane di riscritture — sezione 1).") + print("\n -> PREMESSA CONFERMATA sul feed BTC/ETH, per via diretta e su 62 cicli.") + print(" Rigenerare NON perde la finestra forward; nessuna data di gate si sposta.") + return out + + +# ============================================================================================ +# 1 — RICOSTRUZIONE DELLE TRE SERIE + PROVE D'IDENTITA' +# ============================================================================================ +def _ident_pannello(nome: str, salvato, calcolato) -> float: + d = float(np.max(np.abs(np.asarray(salvato, float) - np.asarray(calcolato, float)))) + print(f" I1 pannello == quello del cron di stanotte ({nome}): max|d| = {d:g}" + f" {'OK' if d == 0.0 else '*** DIVERSO ***'}") + return d + + +def ric_statarb() -> dict: + print("\n 1a. paper_statarb — gate STATARB 2026-09-27") + ps = live("paper_statarb") + st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl") + j = ps.build_joint("1d") + ts, dt, pos, sr = ps._signal(j) + _ident_pannello("pos_modeled", [st["pos_modeled"]], [pos[-1]]) + i0 = int(np.where(ts == st["start_ts"])[0][0]) + + def replay(frame): + sink = Sink() + ps._append = sink + t2, _, p2, _ = ps._signal(frame) + k = int(np.where(t2 == st["start_ts"])[0][0]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, + pos_modeled=float(p2[k]), pos_real=float(p2[k]), + cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL, + peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_trades=0) + ps.advance(s0, frame) + return sink.of("returns.jsonl") + + full = replay(j) + trunc = replay(j.iloc[:-1].reset_index(drop=True)) + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in trunc]) + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, " + f"max|d| = {float(np.max(np.abs(a - b))):g} (l'ultima barra non retroagisce)") + return dict(mod=ps, rec=rec, rep=full, panel=j, extra=dict(pos=pos, ts=ts)) + + +def ric_xsr() -> dict: + print("\n 1b. paper_xsr — gate XSR01 2026-10-23") + px = live("paper_xsr") + st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl") + sink = Sink() + px._append = sink + ts, dt, W, R, rb, syms = px.build_panel() + _ident_pannello("w[modeled]", st["modeled"]["w"], W[-1]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"], + modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])), + reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) + px.advance(s0) + full = sink.of("returns.jsonl") + + # I3: troncare l'INPUT (l'ultima riga di OGNI parquet HL) e ricostruire da capo + orig = px.load_hl + + def load_trunc(sym): + return orig(sym).iloc[:-1] + + px.load_hl = load_trunc + sink2 = Sink() + px._append = sink2 + try: + ts2, _, W2, _, _, syms2 = px.build_panel() + s2 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"], + modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])), + reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS}) + px.advance(s2) + tr = sink2.of("returns.jsonl") + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in tr]) + dmax = float(np.max(np.abs(a - b))) if len(a) == len(b) else float("nan") + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, max|d| = {dmax:g}") + finally: + px.load_hl = orig + return dict(mod=px, rec=rec, rep=full) + + +def ric_dvolspread() -> dict: + print("\n 1c. paper_dvolspread — kill 2026-10-24 / decisione 2027-01-24") + pv = live("paper_dvolspread") + st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text()) + rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl") + P = pv._panel() + _ident_pannello("wb_modeled", [st["wb_modeled"]], [P["wb"][-1]]) + i0 = int(np.where(P["ts"] == st["start_ts"])[0][0]) + + def replay(Q): + sink = Sink() + pv._append = sink + k = int(np.where(Q["ts"] == st["start_ts"])[0][0]) + s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0, + n_flat_signal=0, n_flat_nodata=0, + wb_modeled=float(Q["wb"][k]), wb_real=float(Q["wb"][k]), + cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL, + peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL, + dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN) + out = pv.advance(s0, Q) + return sink.of("returns.jsonl"), out + + full, st_full = replay(P) + Q = {k: (v[:-1] if hasattr(v, "__len__") else v) for k, v in P.items()} + trunc, st_tr = replay(Q) + a = np.array([float(r["net_modeled"]) for r in full[:-1]]) + b = np.array([float(r["net_modeled"]) for r in trunc]) + print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, " + f"max|d| = {float(np.max(np.abs(a - b))):g}") + return dict(mod=pv, rec=rec, rep=full, st_rep=st_tr, st_rec=st) + + +def sezione_1() -> dict: + print("\n" + H) + print(" 1 — RICOSTRUZIONE + PROVE D'IDENTITA' (senza queste, il numero e' un'altra strategia)") + print(H) + print(""" + Tre prove, ognuna chiude un modo diverso di sbagliare: + I1 il pannello che leggo ORA e' quello che il cron ha letto stanotte (i parquet hanno mtime + 00:30-00:35 e nulla li riscrive fino al prossimo giro): si verifica contro lo `state.json` + del monitor, che e' stato scritto DA LUI alle 00:35. + I2 sulle barre che il monitor ha letto GIA' CHIUSE la ricostruzione coincide AL BIT: e' la + parte non affetta dal difetto, ed e' l'unico posto dove i due numeri devono coincidere. + I3 troncare l'ULTIMA barra dell'INPUT da' lo stesso risultato che affettare l'output: nessuna + barra precedente dipende dall'ultima (se dipendesse, la 'riparazione' sarebbe un altro + segnale, non lo stesso).""") + S = dict(statarb=ric_statarb(), xsr=ric_xsr(), dvol=ric_dvolspread()) + + print("\n I2 — barre che il monitor ha letto CHIUSE, e coincidenza al bit:") + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + rec, rep = S[k]["rec"], S[k]["rep"] + d = {int(r["ts"]): float(r["net_modeled"]) for r in rep} + pr = [(int(r["ts"]), float(r["net_modeled"]), d[int(r["ts"])]) for r in rec if int(r["ts"]) in d] + A = np.array([p[1] for p in pr]) + B = np.array([p[2] for p in pr]) + T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pr], unit="ms", utc=True)) + eq = np.abs(A - B) <= 1e-6 + S[k]["pairs"] = (T, A, B) + gg = sorted({str(x.date()) for x in T[eq]}) + note = f" ({gg[0]} -> {gg[-1]})" if gg else "" + print(f" {lab:<18} {int(eq.sum())}/{len(A)} identiche{note}") + if k == "statarb": + print(" quelle 6 sono le barre recuperate dopo l'incidente del feed 09-15/07:") + print(" registrate il 16/07 e riprodotte oggi al bit dopo ~37 riscritture") + print(" notturne -> conferma indipendente della premessa della sezione 0.") + if k == "dvol": + print(" *** ZERO barre non affette: per questo monitor la prova I2 NON ESISTE.") + print(" Restano I1 (pannello identico, max|d|=0), I3 (troncamento innocuo) e il") + print(" fatto strutturale che il replay chiama `advance()` di produzione senza") + print(" una riga riscritta. Va detto: e' una prova piu' debole delle altre due.") + return S + + +# ============================================================================================ +# 2 — I TRE GATE, ALLA LETTERA +# ============================================================================================ +def _riga(lab, m, extra=""): + print(f" {lab:<22} n={m['n']:>3} Sharpe {m['sh']:+7.2f} maxDD {m['dd'] * 100:6.2f}% " + f"ritorno {m['tot'] * 100:+6.2f}% vol {m['sd'] * 100:5.2f}%{extra}") + + +def gate_dvolspread(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2a — DVOLSPREAD (kill 2026-10-24, decisione 2027-01-24) — PER PRIMO: e' l'unico dei") + print(" tre che ha gia' un VETO D'INTEGRITA', e la domanda e' se il difetto lo fa scattare") + print(H) + pv, rec, rep = S["dvol"]["mod"], S["dvol"]["rec"], S["dvol"]["rep"] + A = np.array([float(r["net_modeled"]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] # RIPARATA = solo barre chiuse + mA, mB = mets(A), mets(B) + print(f"\n criterio kill (pre-registrato 26/07): Sharpe forward MODELED < {pv.KILL_SHARPE:+.2f}" + f" -> RITIRO") + _riga("serie ATTUALE", mA) + _riga("serie RIPARATA", mB) + vA = "KILL" if mA["sh"] < pv.KILL_SHARPE else "vivo" + vB = "KILL" if mB["sh"] < pv.KILL_SHARPE else "vivo" + print(f" -> criterio kill: serie attuale {vA} · serie riparata {vB} " + f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}") + print(f" ⚠ ma a n={mA['n']} barre SE(Sharpe) ~ {se_sharpe(mA['n']):.2f}: nessuna delle due") + print(" e' una lettura di gate, e la data del kill e' fra " + f"{(date(2026, 10, 24) - TODAY).days} giorni.") + + print("\n IL VETO D'INTEGRITA' (`n_active + n_flat_signal >= 80% delle barre`):") + stA, stB = S["dvol"]["st_rec"], S["dvol"]["st_rep"] + fA = (stA["n_active"] + stA["n_flat_signal"]) / max(stA["n_bars"], 1) + fB = (stB["n_active"] + stB["n_flat_signal"]) / max(stB["n_bars"], 1) + print(f" serie ATTUALE : attive {stA['n_active']} · flat-da-segnale {stA['n_flat_signal']}" + f" · flat-SENZA-DATO {stA['n_flat_nodata']} -> valide {fA:.0%} " + f"(soglia {pv.MIN_ACTIVE_FRAC:.0%}) -> {'VETO' if fA < pv.MIN_ACTIVE_FRAC else 'PASSA'}") + print(f" serie RIPARATA: attive {stB['n_active']} · flat-da-segnale {stB['n_flat_signal']}" + f" · flat-SENZA-DATO {stB['n_flat_nodata']} -> valide {fB:.0%} -> " + f"{'VETO' if fB < pv.MIN_ACTIVE_FRAC else 'PASSA'}") + print(""" + 📌 IL RISULTATO E' SUL VETO, NON SUL MONITOR. Una serie che misura DUE MINUTI di mercato al + giorno passa al 100% un veto d'integrita' progettato apposta per non far decidere su dati + mancanti. Non e' un difetto di taratura: e' che il veto e' costruito sulla domanda giusta + per un altro guasto. Conta *se il DVOL c'era* (e c'era, 28/28) e non puo' vedere *quanto + dura la barra su cui c'era* — sono due assi ortogonali. La stessa soglia 0.80 applicata alla + quota di barre CHIUSE leggerebbe 0/28 = 0% e vieterebbe la decisione. + E' la stessa forma gia' pagata dal progetto tre volte: 'una riga presente non e' un dato + presente'. Qui la variante nuova e': *una barra presente non e' una GIORNATA presente*.""") + + print("\n criteri (b) marginale ADDS / (c) deflated-Sharpe / (d) weights_tilt_null: NON GIRATI,") + print(" e il motivo e' una misura, non pigrizia: si valutano sul campione ESTESO (in-sample") + print(f" attivo 1949 barre + forward). Oggi il forward pesa {mA['n']}/{1949 + mA['n']} = " + f"{100 * mA['n'] / (1949 + mA['n']):.1f}% del campione; alla decisione del 24/01/2027 ne") + print(" pesera' ~8%. -> il difetto NON puo' ribaltare (c): puo' ribaltare solo (a), che e'") + print(" calcolato sulla sola finestra forward ed e' interamente determinato da lui.") + return vA, vB + + +def gate_statarb(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2b — STATARB (decisione 2026-09-27) — il gate su cui il difetto MORDE") + print(H) + rec, rep = S["statarb"]["rec"], S["statarb"]["rep"] + gate = importlib.import_module("r0724_statarb_deploy_gate") + A = np.array([float(r["net_modeled"]) for r in rec]) + Ar = np.array([float(r["net_real"]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + Br = np.array([float(r["net_real"]) for r in rep])[:-1] + mA, mB = mets(A), mets(B) + hc = lambda m, r: abs(float((np.prod(1 + m) - np.prod(1 + r)) * 100)) + hA, hB = hc(A, Ar), hc(B, Br) + print(f"\n soglie pre-registrate 24/07: Sharpe >= {gate.SH_DEPLOY} · maxDD < {gate.DD_MAX:.0%}" + f" · fill-haircut < {gate.HAIRCUT_MAX_PP} pp") + _riga("serie ATTUALE", mA, f" haircut {hA:.3f} pp") + _riga("serie RIPARATA", mB, f" haircut {hB:.3f} pp") + + def verdetto(sh, dd, h): + if sh >= gate.SH_DEPLOY and dd < gate.DD_MAX and h < gate.HAIRCUT_MAX_PP: + return "CANDIDATO AL DEPLOY" + if sh >= 0: + return "ESTENSIONE (unica, al 2026-12-26)" + return "RITIRO" + vA, vB = verdetto(mA["sh"], mA["dd"], hA), verdetto(mB["sh"], mB["dd"], hB) + for lab, sh, dd, h, v in (("ATTUALE", mA["sh"], mA["dd"], hA, vA), + ("RIPARATA", mB["sh"], mB["dd"], hB, vB)): + print(f" {lab:<9} Sharpe>={gate.SH_DEPLOY}: {'SI' if sh >= gate.SH_DEPLOY else 'NO':<3}" + f" | maxDD<{gate.DD_MAX:.0%}: {'SI' if dd < gate.DD_MAX else 'NO':<3}" + f" | haircut<{gate.HAIRCUT_MAX_PP}pp: {'SI' if h < gate.HAIRCUT_MAX_PP else 'NO':<3}" + f" -> {v}") + print(f"\n -> DUE CRITERI SU TRE SI RIBALTANO. Sharpe {mA['sh']:+.2f} -> {mB['sh']:+.2f} " + f"(attraversa 0 e la soglia 0.5); maxDD {mA['dd']:.1%} -> {mB['dd']:.1%} (attraversa la") + print(" guardia del 10%). Il terzo (haircut) non si ribalta e non poteva: e' un") + print(" rapporto fra due libri che subiscono lo STESSO troncamento.") + + print("\n DIAGNOSTICA PRE-REGISTRATA IL 25/07 (statica sempre-short a pari vol-target):") + bA = gate._bench_static_short(rec) + bB = gate._bench_static_short(rep[:-1]) + if bA and bB: + print(f" benchmark sulla finestra ATTUALE : {bA[0]:+.2f} su {bA[1]} barre") + print(f" benchmark sulla finestra RIPARATA: {bB[0]:+.2f} su {bB[1]} barre") + print(f" STATARB - benchmark: attuale {mA['sh'] - bA[0]:+.2f} " + f"riparata {mB['sh'] - bB[0]:+.2f}") + print(""" + 📌 ASIMMETRIA CHE VALE PIU' DEI NUMERI: il benchmark NON e' affetto dal difetto — e' + ricalcolato dal pannello, non letto dalla serie registrata (varia di %+.2f fra le due + finestre, contro %+.2f della strategia). Alla data del gate l'operatore metterebbe quindi + a confronto un benchmark GIUSTO con un numero di strategia SBAGLIATO, e la diagnostica + sembrerebbe piu' favorevole di quanto sia: +%.2f invece di +%.2f di margine. + Il segno del margine sopravvive comunque (STATARB batte la statica in entrambe le lenti): + cio' che il difetto ribalta e' il gate, non questa diagnostica.""" % + (bB[0] - bA[0], mB["sh"] - mA["sh"], mA["sh"] - bA[0], mB["sh"] - bB[0])) + else: + print(" benchmark NON calcolabile -> non girato.") + return vA, vB + + +def gate_xsr(S: dict) -> tuple[str, str]: + print("\n" + H) + print(" 2c — XSR01 (decisione 2026-10-23)") + print(H) + rec, rep = S["xsr"]["rec"], S["xsr"]["rep"] + gate = importlib.import_module("r0725_xsr_deploy_gate") + k5 = "net_REAL-$5000" + A = np.array([float(r["net_modeled"]) for r in rec]) + A5 = np.array([float(r[k5]) for r in rec]) + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + B5 = np.array([float(r[k5]) for r in rep])[:-1] + mA, mB = mets(A), mets(B) + + def haircut(m, r5): + tm = float(np.prod(1 + m) - 1) + t5 = float(np.prod(1 + r5) - 1) + return (tm - t5) / abs(tm) if tm != 0 else float("nan") + hA, hB = haircut(A, A5), haircut(B, B5) + print(f"\n soglie pre-registrate 25/07: Sharpe >= {gate.SH_DEPLOY} E haircut$5000 <= " + f"{gate.HAIRCUT_MAX:.0%} · ritiro sotto Sharpe {gate.SH_RETIRE}") + print(f" ⚠ se l'haircut sfonda -> RITIRO a prescindere dallo Sharpe (regola esplicita)") + _riga("serie ATTUALE", mA, f" haircut$5000 {hA * 100:+.1f}%") + _riga("serie RIPARATA", mB, f" haircut$5000 {hB * 100:+.1f}%") + + def verdetto(sh, h): + if np.isfinite(h) and h > gate.HAIRCUT_MAX: + return "RITIRO (haircut)" + if sh >= gate.SH_DEPLOY: + return "CANDIDATO SLEEVE" + if sh >= gate.SH_RETIRE: + return "ESTENSIONE (unica, al 2027-01-21)" + return "RITIRO" + vA, vB = verdetto(mA["sh"], hA), verdetto(mB["sh"], hB) + print(f" ATTUALE -> {vA}\n RIPARATA -> {vB} " + f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}") + print(f"\n -> il verdetto NON si ribalta, ma per un caso: entrambe stanno sotto " + f"{gate.SH_RETIRE}. Il") + print(f" numero pero' cambia di {abs(mA['sh'] / mB['sh']):.1f}x ({mA['sh']:+.2f} contro " + f"{mB['sh']:+.2f}), e con SE ~ {se_sharpe(mA['n']):.1f} nessuno dei due decide niente.") + print(" L'haircut e' l'unico criterio robusto al difetto (rapporto fra libri troncati") + print(" allo stesso modo) ed e' lontanissimo dalla guardia in entrambe le lenti.") + return vA, vB + + +# ============================================================================================ +# 3 — IL MECCANISMO: perche' il troncamento non e' rumore +# ============================================================================================ +def sezione_3(S: dict) -> None: + print("\n" + H) + print(" 3 — PERCHE' IL DIFETTO NON E' RUMORE: comprime la VOL piu' della MEDIA") + print(H) + print(""" + Un troncamento a k minuti su 1440 scala la media del rendimento circa come k e la deviazione + standard come sqrt(k) (random walk). Quindi il rapporto media/sd — cioe' lo Sharpe — viene + MOLTIPLICATO per ~sqrt(k/1440)... in valore assoluto no: il numeratore e' la media del pezzo + troncato, che non ha lo stesso SEGNO della media della giornata intera. Risultato: la serie + registrata produce letture di Sharpe piu' ESTREME e in una direzione che non e' quella vera. + Si vede in tabella: la vol registrata e' una frazione di quella vera, ma il |Sharpe| non e' piu' + piccolo — in due casi su tre e' molto piu' GRANDE.""") + print(f"\n {'monitor':<20} {'vol REG':>9} {'vol RIP':>9} {'min/giorno':>11} " + f"{'Sh REG':>8} {'Sh RIP':>8} nota") + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + T, A, B = S[k]["pairs"] + Bc = B[:-1] if len(B) == len(A) else B + # ⚠ l'INTERA finestra dell'incidente del feed (09-16/07) va esclusa dalla stima dei + # minuti/giorno, non le sole barre risultate identiche: durante il blocco il monitor non + # trovava barre nuove e al ripristino ne ha recuperate 7 in un colpo, alcune solo + # PARZIALMENTE riallineate. Escludendo le sole identiche la stima esce 4x piu' alta + # (16,7 invece di 3,9 min/giorno su statarb) — e' la stessa maschera del 22/08. + keep = ~((T >= FREEZE[0]) & (T <= FREEZE[1])) + mA, mB = mets(A), mets(Bc) + kA, kB = mets(A[keep]), mets(B[:len(A)][keep]) + minuti = (kA["sd"] / kB["sd"]) ** 2 * 1440.0 + nota = ("SEGNO OPPOSTO" if np.sign(mA["sh"]) != np.sign(mB["sh"]) + else f"|Sh| x{abs(mA['sh']) / abs(mB['sh']):.1f}") + print(f" {lab:<20} {mA['sd'] * 100:8.2f}% {mB['sd'] * 100:8.2f}% {minuti:10.1f} " + f"{mA['sh']:+8.2f} {mB['sh']:+8.2f} {nota}" + + (f" ({int((~keep).sum())} barre dell'incidente escluse)" if (~keep).any() else "")) + print(""" + Il caso peggiore NON e' quello col |Sharpe| piu' gonfiato: e' `paper_statarb`, dove il + |Sharpe| e' quasi identico (1,96 contro 2,02) e cambia il SEGNO. Un controllo che cercasse + "numeri troppo belli" lo lascerebbe passare senza una parola: e' proprio il monitor che + alimenta il gate piu' vicino. + -> la firma da cercare non e' un |Sharpe| alto ma una VOL FORWARD molto sotto quella del + backtest: 7,9% contro ~31%, 0,5% contro ~2,8%, 0,8% contro ~19%. + ⚠ i "min/giorno" sono una LETTURA del rapporto di varianza (assume random walk + sull'intervallo troncato), non un cronometro, e sono sensibili alla maschera: su statarb + 3,9 escludendo l'incidente del feed, 16,7 escludendo le sole barre identiche, 92 + includendo tutto. Si cita la prima, che e' quella del 22/08; l'ordine di grandezza e' + quello, la cifra no. + ✅ Replica indipendente del filone MONITOR-AUDIT: 3,9 / 41,2 / 2,5 contro i 4 / 41 / 2 + pubblicati stamattina, per un percorso scritto separatamente.""") + + print("\n `implausible_sharpe` puntato sulle serie forward (mai fatto — nota del 22/08).") + print(" ⚠ il gate chiede n>=30: due serie forward su tre NON sono giudicabili, e questo") + print(" e' esso stesso un risultato — il gate esiste dal 26/07 ma non puo' sorvegliare") + print(" una finestra forward giovane, che e' esattamente quando serve.") + import altlib as al + for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")): + T, A, B = S[k]["pairs"] + for nome, v in (("REGISTRATA", A), ("RIPARATA", B[:-1] if len(B) == len(A) else B)): + idx = T[:len(v)] + try: + r = al.implausible_sharpe(pd.Series(v, index=idx)) + corto = any("troppo corto" in x for x in r["reasons"]) + flag = ("SEGNALA" if r["implausible"] + else "n/giudic." if corto else "ok") + print(f" {lab:<18} {nome:<11} {flag:<10} {'; '.join(r['reasons'])[:66]}") + except Exception as e: # noqa: BLE001 + print(f" {lab:<18} {nome:<11} non girato ({type(e).__name__})") + + +# ============================================================================================ +# 4 — QUANTO COSTA ASPETTARE +# ============================================================================================ +GATES = ( + ("STATARB", "statarb", date(2026, 9, 27)), + ("XSR01", "xsr", date(2026, 10, 23)), + ("DVOLSPREAD", "dvol", date(2026, 10, 24)), +) + + +def sezione_4(S: dict) -> None: + print("\n" + H) + print(" 4 — QUANTO COSTA ASPETTARE (l'unico argomento quantitativo su QUANDO riparare)") + print(H) + print(""" + Due scenari, e la differenza fra loro e' tutto il contenuto della sezione: + RIPARA+RIGENERA : si corregge `advance()` e si RICALCOLA la finestra dall'inception. + Possibile perche' le barre chiuse non vengono riscritte (sezione 0). + RIPARA IN AVANTI : si corregge `advance()` e basta. Le barre gia' scritte restano sbagliate.""") + print(f"\n {'gate':<12} {'data':<12} {'barre al gate':>14} {'oggi':>6} " + f"{'corrette se si ripara OGGI':>28} {'se si ripara alla vigilia':>27}") + for nome, key, gd in GATES: + rec = S[key]["rec"] + first = pd.Timestamp(rec[0]["dt"][:10]).date() + n_gate = (gd - first).days + 1 + n_oggi = len(rec) + print(f" {nome:<12} {str(gd):<12} {n_gate:>14} {n_oggi:>6} " + f"{f'{n_gate - n_oggi} ({(n_gate - n_oggi) / n_gate:.0%})':>28} " + f"{f'1 ({1 / n_gate:.0%})':>27}") + print("\n Con RIGENERAZIONE la colonna giusta e' un'altra: 100% a QUALUNQUE data di") + print(" riparazione. Il costo dell'attesa e' ZERO barre — cio' che si paga aspettando non") + print(" e' dato perso ma il RISCHIO che la riparazione non arrivi prima del gate.") + print(" Senza rigenerazione, invece, riparare oggi lascia comunque sbagliata la maggioranza") + print(" della finestra di STATARB, e riparare alla vigilia non serve a niente.") + + print("\n SENSIBILITA' — quanto deve essere contaminata la finestra perche' il verdetto") + print(" cambi. Misurata sui dati di OGGI, non estrapolata: si costruisce la serie mista") + print(" `registrate[:k] + riparate[k:]` (= riparazione in avanti dal giorno k) e si legge") + print(" il criterio. k=0 e' la rigenerazione completa, k=n e' la serie di oggi.") + gate = importlib.import_module("r0724_statarb_deploy_gate") + rec, rep = S["statarb"]["rec"], S["statarb"]["rep"] + A = np.array([float(r["net_modeled"]) for r in rec])[:-1] + B = np.array([float(r["net_modeled"]) for r in rep])[:-1] + n = len(A) + print(f"\n STATARB — {n} barre chiuse; soglia deploy {gate.SH_DEPLOY}, ritiro sotto 0, " + f"guardia maxDD {gate.DD_MAX:.0%}") + print(f" {'corrette (coda)':>16} {'contaminate':>12} {'Sharpe':>8} {'maxDD':>8} verdetto") + verd = [] + for k in range(0, n + 1): + m = mets(np.concatenate([A[:k], B[k:]])) + v = ("CANDIDATO" if (m["sh"] >= gate.SH_DEPLOY and m["dd"] < gate.DD_MAX) + else "ESTENSIONE" if m["sh"] >= 0 else "RITIRO") + verd.append((k, m, v)) + # minimo numero di barre CORRETTE in coda che basta a leggere gia' il verdetto riparato + v_rip = verd[0][2] + min_ok = min((n - k for k, m, v in verd if v == v_rip), default=n) + for k, m, v in verd: + if k in (0, n // 4, n // 2, (3 * n) // 4, n - min_ok, n - min_ok + 1, n): + print(f" {n - k:>16} {f'{k} ({k / n:.0%})':>12} {m['sh']:+8.2f} " + f"{m['dd'] * 100:7.2f}% {v}") + n_res = 90 - len(S["statarb"]["rec"]) # barre che restano da qui al gate del 27/09 + print(f""" + 📌 LA RIPARAZIONE PARZIALE NON E' PROPORZIONALE. Su questo campione bastano + **{min_ok} barre corrette in coda su {n}** ({min_ok / n:.0%}) perche' il criterio legga gia' il + verdetto della serie riparata. Il meccanismo e' quello della sezione 3: una barra troncata a + ~4 minuti su 1440 porta ~1/370 della varianza E ~1/370 del rendimento di una barra vera + (entrambi scalano con la frazione di giornata), quindi pesa quasi nulla sia nella media sia + nella deviazione della miscela. La maggioranza NUMERICA delle barre non comanda. + Detto meglio, ed e' la formulazione onesta: riparare in avanti senza rigenerare non + CONTAMINA la finestra, la ACCORCIA — il criterio finisce per leggere la strategia sulle sole + barre corrette. Riparando oggi, al 27/09 STATARB avrebbe {n_res} barre vere su 90: una + finestra piu' corta ma VERA, con SE(Sharpe) ~{se_sharpe(n_res):.1f} invece di ~{se_sharpe(90):.1f}. + Riparare alla vigilia (1 barra) no: li' il numero resta quello di oggi. + ⚠ Il {min_ok}/{n} NON e' una costante — dipende da quali barre capitano in coda, e cambia col + campione: si legge come "poche", non come una soglia. E rigenerare resta strettamente + migliore, per due cose che l'accorciamento non da': un maxDD leggibile (la miscela ha un + percorso di equity che non e' mai esistito) e una finestra rileggibile con altri criteri.""") + + +# ============================================================================================ +def main() -> None: + print(H) + print(" GATE-RECON — cosa direbbero i tre gate pre-registrati con le serie riparate") + print(f" {TODAY} SOLA LETTURA su produzione (nessun file di data/ aperto in scrittura)") + print(H) + sezione_0() + S = sezione_1() + v_dv = gate_dvolspread(S) + v_st = gate_statarb(S) + v_xs = gate_xsr(S) + sezione_3(S) + sezione_4(S) + + print("\n" + H) + print(" RIEPILOGO — il DELTA, non la decisione (i gate hanno una data e non si anticipano)") + print(H) + print(f" STATARB 27/09 — serie riparata: {v_st[1]:<36} · serie attuale: {v_st[0]}") + print(f" XSR01 23/10 — serie riparata: {v_xs[1]:<36} · serie attuale: {v_xs[0]}") + print(f" DVOLSPREAD 24/10 — serie riparata: {v_dv[1]:<36} · serie attuale: {v_dv[0]}") + print("\n Nessuno di questi e' il verdetto del gate: sono i criteri letti OGGI, su una finestra") + print(" ancora incompleta. Servono a una sola decisione, che e' dell'operatore: riparare") + print(" `advance()` e RIGENERARE prima del 27/09, oppure sapere che quel giorno il criterio") + print(" verra' letto su una serie che misura 4 minuti di mercato al giorno.") + + +if __name__ == "__main__": + main()