From fbb6e75e67e9dc586c3ec847a312fe6e855335f5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:52:53 +0000 Subject: [PATCH] =?UTF-8?q?research(wave-0822):=20MONITOR-AUDIT=20?= =?UTF-8?q?=E2=80=94=204=20monitor=20su=206=20rotti,=20il=20gate=20STATARB?= =?UTF-8?q?=20si=20ribalta,=20ma=20la=20finestra=20e'=20rigenerabile=20e?= =?UTF-8?q?=20nessuna=20data=20si=20sposta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 48 ++++++ scripts/research/r0822_monitor_audit.py | 209 +++++++++++++++++++----- scripts/research/r0822_prop_alloc.py | 32 ++-- 3 files changed, 234 insertions(+), 55 deletions(-) diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d78570b..1918bf3 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -21,6 +21,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato. | — | **SLIP-AUDIT** (audit di fatto) | **SCARTATO** = nessun costo nascosto a questa taglia | i backtest a 10 bps RT restano **conservativi di ~1,5 bps/lato**; nessuna evidenza di impatto sopravvive al null (p=0,274; estremi avversi **1/18 contro 4,7 attesi**). **Ma la misura ha una data di scadenza** | | — | **SCETTICO su GROWTH-POLICY** | **REGGE** (condizione bloccante DECADUTA) | la lente wick accoppiata **non cambia il gradino**: e' un ricarico moltiplicativo **costante ~3,5%** sul maxDD, non un'amplificazione (Δ/k **cala**). Ma su una regola a **UN giorno** close-only e' **esattamente cieca** (rapporto INF) -> la lezione 25/07 e' vera e **non si trasferisce** | | 13 | BASIS-CALENDAR | **SCARTATO** + **1 dataset nuovo** | il basis dei futures datati **E' il funding del perp quotato diversamente** (7,33% contro 6,48%/anno): non e' un quarto lato del filone funding, e' lo stesso lato. Premio incassabile **+0,85%/anno con IC95 che contiene lo zero** | +| — | **MONITOR-AUDIT** (produzione) | 🚨 **4 monitor su 6 rotti**, ma **la finestra NON e' persa** | `paper_statarb` registra **4 minuti al giorno** e il suo gate del **27/09 si ribalta su 2 criteri su 3** (Sharpe registrato **+1,96** contro **−2,02** ricostruito). **La riparazione e' "riparare + RIGENERARE", non "riparare e azzerare": nessuna data di gate si sposta** | ## Note che sopravvivono ai singoli filoni @@ -590,3 +591,50 @@ formula ma il binario**: meno gradi di liberta', **dimezza gli ordini** (miglior $635), 115% sul FULL — ma paga in fragilita' d'ancora (20/23 contro 23/23, banda che include −0,061, hold-out +0,072 contro +0,101). **Resta un terzo della fortuna d'ancora del libro stesso (+0,196): niente qui giustifica di toccare pesi, cron o config.** + +### MONITOR-AUDIT (r0822_monitor_audit.py) — audit di PRODUZIONE, 6 monitor, 4,7 s +✅ **Metodo che rende il risultato non contestabile:** l'audit **riesegue `advance()` di produzione +tale e quale**, sostituendo solo `_append` con un raccoglitore in memoria -> nessuna +reimplementazione, quindi **nessun drift fra audit e monitor**. Sola lettura verificata (gli mtime +di `data/paper_*` sono rimasti quelli del cron delle 00:35). +| monitor | barra parziale | barre coincidenti | corr | **min/giorno misurati** | gate | ricostruibile | +|---|---|---|---|---|---|---| +| `paper_statarb` | **SI'** | **0/46** | +0,297 | **4** | **STATARB 27/09** | **SI', bit-exact** | +| `paper_dvolspread` | **SI'** | 0/28 | +0,262 | **2** | DVOLSPREAD 24/10 | SI', bit-exact | +| `paper_xsr` | **SI'** | 1/28 | −0,045 | **41** | XSR01 23/10 | SI', bit-exact | +| `paper_portfolio` | **SI'** | 7/56 | +0,245 | 19 | — (dashboard) | si', non al bit | +| `paper_prevday` | si' (1 barra su 24) | **1259/1319** | **+0,997** | 1.438 | — | si', bit-exact | +| `paper_combo` | **NO** | 7/37 | +0,991 | 1.402 | — | si', non al bit | +📌 **La patologia e' la stessa, la TAGLIA varia di un fattore 20** — e dipende da quanto pesa la +barra piu' recente nel segnale, esattamente come la lezione del 26/07 su SKH01 contro TP01. +🚨 **Il gate STATARB del 27/09 si ribalta su DUE criteri su tre:** Sharpe registrato **+1,96** (sopra +la soglia 0,5) contro **−2,02** ricostruito; maxDD **2,2% contro 15,3%** (guardia <10%). *Fra 36 +giorni si sarebbe deciso su una serie che misura 4 minuti di mercato al giorno.* +⚠️ XSR01 e DVOLSPREAD danno lo **stesso verdetto** oggi, ma su numeri implausibili (Sharpe −15,8 su +28 barre = oltre 4 SE). **`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato sulle +serie forward.** +✅ **LA BUONA NOTIZIA, ed e' misurata: la finestra forward NON va persa.** Il replay *e'* la serie +corretta sulla stessa finestra, con config congelata e codice di produzione. Le due condizioni sono +**provate due volte in modo indipendente**: (a) `paper_prevday` ha **1427/1488 barre identiche al +bit** dopo 62 notti di riscrittura (il feed **non tocca** le barre chiuse); (b) le 6 barre di +`paper_statarb` recuperate dopo un guasto coincidono **al bit**. +-> **La decisione non e' "riparare e azzerare" ma "riparare `advance()` + RIGENERARE", e nessuna +delle tre date di gate si sposta.** Anche nell'ipotesi pessimista (`--reset`) l'unico gate che +slitterebbe e' STATARB, di **+18 giorni**. +📌 **Dettaglio che vale una regola: le uniche barre corrette di `paper_statarb` esistono perche' il +feed si e' ROTTO.** `rebuild_history` e' abortito per 7 giri consecutivi (09-15/07, EPERM sul +backup, gia' riparato); al ripristino il monitor ha recuperato 7 barre in un colpo, **6 delle quali +gia' chiuse** -> coincidono col replay al bit. *Il guasto ha prodotto da solo il controllo che +serviva a diagnosticare l'altro guasto.* +📌 **`paper_combo` e' sano PER CASO:** aspetta la chiusura di una **borsa**, non per progetto. +Nessuna riga di codice chiede la barra chiusa — **se la sua griglia cambiasse diventerebbe rotto in +silenzio.** +✅ **Guardia raccomandata (NON implementata), e ha il pregio di essere banale:** +`ts_ultima_barra + cadenza <= mtime del file di serie`, grazia 5 min. **O(1)**, nessuna strategia da +rieseguire, usa solo cio' che il monitor gia' scrive; segnala **5/6** e **tace sul sano**; controllo +positivo sintetico **6/6 nei due versi**. Il vol-ratio va **dopo, non al posto**: copre un guasto +diverso (drift monitor-strategia) e **non vede `paper_prevday`** (rapporto 0,999). +⚠️ Limiti dichiarati: i "minuti/giorno" sono una lettura del **rapporto di varianza** (assume random +walk sull'intervallo troncato), non un cronometro — il cronometro diretto e' 32-35 min; e per +`paper_portfolio`/`paper_combo` la ricostruzione **non sara' al bit** perche' la gamba GTAA legge +`ADJUSTED_LAST` di IB, ri-aggiustato all'indietro. diff --git a/scripts/research/r0822_monitor_audit.py b/scripts/research/r0822_monitor_audit.py index 5f30ecd..9cf2f57 100644 --- a/scripts/research/r0822_monitor_audit.py +++ b/scripts/research/r0822_monitor_audit.py @@ -9,7 +9,7 @@ vol registrata 0,46% contro 2,74% ricalcolata. Il difetto non e' di XSR01: e' della FORMA `new = [i for i in range(len(ts)) if ts[i] > last_ts]` applicata a un pannello la cui ultima riga e' provvisoria. Quella forma e' copiata in sei file. Tre decisioni PRE-REGISTRATE si leggono su queste serie: - STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.35) + STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.5) XSR01 2026-10-23 (r0725_xsr_deploy_gate: Sharpe >= 1.0 E haircut $5.000 <= 40%) DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread) e `monitor_health` dichiara **OK** tutti e sei, perche' misura freschezza e buchi: una serie @@ -42,11 +42,18 @@ Poi: E. IL COSTO DELLA RIPARAZIONE per gate: giorni di finestra persi, data in cui il contatore nuovo tornerebbe alla stessa numerosita', e se esiste una RICOSTRUZIONE a posteriori. -COSA MI ASPETTAVO PRIMA DI MISURARE: che i tre monitor giornalieri su feed crypto (xsr, statarb, -dvolspread) avessero la stessa patologia identica; che `paper_prevday`, essendo su griglia 1h con -24 barre al giorno, ne avesse una MOLTO piu' mite (una barra su 24 troncata); e che -`paper_combo`, che vive sul calendario di borsa, fosse o rotto come gli altri o sano per caso. -Solo la terza aspettativa e' stata una sorpresa. +COSA MI ASPETTAVO PRIMA DI MISURARE, e cosa e' andato diversamente: + * i tre monitor giornalieri su feed crypto (xsr, statarb, dvolspread) con la STESSA patologia, + intorno ai ~35 minuti al giorno di XSR01 -> **meta' giusto**: la patologia e' la stessa ma la + TAGLIA no (2, 4 e 41 min/giorno). Un libro dollar-neutral perde molto piu' di un libro + direzionale a parita' di troncamento, perche' su 35 minuti le due gambe si muovono insieme; + * `paper_prevday` MOLTO piu' mite (1 barra su 24) -> **confermato** (1.438 min/giorno su 1.440), + con un dettaglio non previsto: il difetto si propaga alla barra successiva quando il bersaglio + calcolato sulla barra parziale differisce; + * `paper_combo` rotto o sano per caso -> **sano, e per caso**: aspetta la chiusura di una BORSA; + * NON previsto: le uniche barre corrette di `paper_statarb` esistono perche' il feed si ROMPE + per 7 giorni a luglio e il monitor le recupera CHIUSE. Sono anche la prova che serviva per + dire che la finestra e' ricostruibile. nice -n 19 timeout 900 uv run python scripts/research/r0822_monitor_audit.py """ @@ -289,7 +296,8 @@ def b_xsr(): print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.") out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs])) RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23", - src="hl_*_1d.parquet", cad_h=24.0, ric="SI (bit-exact)") + src="hl_*_1d.parquet", cad_h=24.0, ric="SI bit-exact", + rec=rec, rep=sink.of("returns.jsonl")) def b_statarb(): @@ -323,17 +331,18 @@ def b_statarb(): print(f"\n 📌 LE UNICHE BARRE CORRETTE DELLA SERIE VENGONO DA UN GUASTO: le {int(ident.sum())} " f"identiche\n sono CONSECUTIVE ({gg[0]} -> {gg[-1]}) e cadono dentro l'incidente " "del feed.") - ev = freeze_evidence() + ev = sorted(freeze_evidence()) print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in " - f"{len(ev)} giri\n ({', '.join(sorted(ev)[:8])}{'...' if len(ev) > 8 else ''}) — " - "EPERM sul backup, difetto gia' riparato\n nel sorgente. Col feed fermo il monitor " - "non trovava barre nuove; al ripristino ne ha\n recuperate 7 in un colpo solo, di " - "cui 6 GIA' CHIUSE -> coincidono col replay al bit.") + f"{len(ev)} giri consecutivi") + print(f" ({ev[0]} -> {ev[-1]}), EPERM sul backup — difetto gia' riparato nel sorgente.") + print(" Col feed fermo il monitor non trovava barre nuove; al ripristino ne ha") + print(" recuperate 7 in un colpo solo, di cui 6 GIA' CHIUSE -> coincidono al bit.") print(" Due conseguenze: (a) il 'min/giorno' globale e' GONFIATO da quelle barre — la") print(" riga 'ESCLUSO l'incidente' e' la stima onesta; (b) quelle 6 barre sono la prova") print(" che, su barre chiuse, il codice di produzione riproduce il replay ESATTAMENTE.") RES["paper_statarb"] = dict(parziale="SI", cmp=out["clean"], gate="STATARB 2026-09-27", - src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI (bit-exact)") + src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI bit-exact", + rec=rec, rep=sink.of("returns.jsonl")) def b_dvolspread(): @@ -374,7 +383,7 @@ def b_dvolspread(): RES["paper_dvolspread"] = dict(parziale="SI", cmp=out["clean"], gate="DVOLSPREAD 2026-10-24 / 2027-01-24", src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0, - ric="SI (bit-exact)") + ric="SI bit-exact", rec=rec, rep=sink.of("returns.jsonl")) def b_prevday(): @@ -429,7 +438,7 @@ def b_prevday(): print(" 📌 E il verdetto operativo e' l'opposto degli altri quattro: su griglia oraria il difetto") print(" c'e' ma vale ~0.1% della varianza. `paper_prevday` NON e' da rigenerare.") RES["paper_prevday"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (lead in monitor)", - src="btc/eth 1h grezzo", cad_h=1.0, ric="SI (bit-exact)") + src="btc/eth 1h grezzo", cad_h=1.0, ric="SI bit-exact") def b_portfolio(): @@ -457,8 +466,9 @@ def b_portfolio(): if ident.any(): print(f"\n ⚠ TRAPPOLA DA NON PRENDERE: delle {int(ident.sum())} barre 'identiche', " f"{int(zeri.sum())} sono ZERO contro ZERO") - print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ") = giorni in cui TUTTI e cinque") - print(" gli sleeve erano flat (TP01 risk-off, GTAA a borsa chiusa, VRP fuori scadenza).") + print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ")") + print(" = giorni in cui TUTTI e cinque gli sleeve erano flat insieme (TP01 risk-off,") + print(" GTAA a borsa chiusa nel weekend, VRP fuori scadenza).") print(" Contarle come 'coincidono' sarebbe la trappola gia' codificata dal progetto —") print(" barre ATTIVE, non barre di calendario (26/07, il gate che segnalo' VRP01 perche'") print(" contava il 94% di zeri). Su barre attive le coincidenze qui sono " @@ -474,7 +484,7 @@ def b_portfolio(): RES["paper_portfolio"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (dashboard/book di ricerca)", src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0, - ric="SI* (equity IB ri-aggiustata)") + ric="SI non al bit") def b_combo(): @@ -514,7 +524,7 @@ def b_combo(): print(" senza che nulla lo segnali.") RES["paper_combo"] = dict(parziale="NO", cmp=out["clean"], gate="nessuno (paper cross-venue)", src="TP01 1h->1d su griglia IB", cad_h=24.0, - ric="SI* (equity IB ri-aggiustata)") + ric="SI non al bit") # ============================================================================================ @@ -522,7 +532,7 @@ def b_combo(): # ============================================================================================ GATE_INFO = { "paper_xsr": ("XSR01 2026-10-23", "Sharpe>=1.0 E haircut $5.000<=40%"), - "paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.35"), + "paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.5"), "paper_dvolspread": ("DVOLSPREAD 24/10 kill", "Sharpe<-0.50 -> ritiro; 27/01 decisione piena"), "paper_prevday": ("-", "lead senza data"), "paper_portfolio": ("-", "dashboard"), @@ -537,22 +547,105 @@ def sezione_C(): print("\n Le colonne sono calcolate ESCLUDENDO l'incidente del feed 09-15/07 (le barre") print(" recuperate a posteriori sono chiuse e attenuerebbero il difetto per la ragione") print(" sbagliata). 'min/g' = minuti di mercato al giorno effettivamente misurati, su 1440.") - print(f"\n {'monitor':<19}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}" - f"{'ricostruibile':>25} gate dipendente") + print(f"\n {'monitor':<19}{'barra parz.':>13}{'ident./tot':>13}{'corr':>9}{'min/g':>8}" + f"{'ricostruibile':>17} gate dipendente") for k, v in RES.items(): c = v["cmp"] ratio = "{}/{}".format(c["ident"], c["n"]) - print(f" {k:<19}{v['parziale']:>6}{ratio:>13}{c['corr']:>+9.3f}" - f"{c['minuti']:>9,.0f}{v['ric']:>25} {GATE_INFO[k][0]}") + print(f" {k:<19}{v['parziale']:>13}{ratio:>13}{c['corr']:>+9.3f}" + f"{c['minuti']:>8,.0f}{v['ric']:>17} {GATE_INFO[k][0]}") print("\n 'ricostruibile' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare") - print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script. '*' =") - print(" ricostruibile ma NON al bit, perche' una gamba viene dal feed equity IB che e'") - print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`).") + print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script.") + print(" 'non al bit' = ricostruibile, ma una gamba viene dal feed equity IB, che e'") + print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`) -> i centesimi non torneranno uguali.") print("\n LETTURA IN UNA RIGA: quattro monitor su sei registrano fra 2 e 41 minuti di mercato") print(" al giorno spacciandoli per 24 ore; i due che non lo fanno sono quello a griglia oraria") print(" (dove il difetto vale una barra su 24) e quello che aspetta la chiusura di una BORSA.") +def sezione_C2(): + """Il criterio di ciascun gate PRE-REGISTRATO resta leggibile? Le soglie NON sono ridichiarate + qui: si IMPORTANO dagli script di gate e dal monitor (regola del progetto — un sorvegliante + deriva il proprio bersaglio dal codice sorvegliato, mai lo ridichiara: 21/08, `fee_watch`).""" + print("\n" + "=" * 102) + print(" C2 — IL CRITERIO DEI TRE GATE PRE-REGISTRATI E' ANCORA LEGGIBILE?") + print("=" * 102) + import r0724_statarb_deploy_gate as GS # noqa: E402 (solo costanti: ha il main-guard) + import r0725_xsr_deploy_gate as GX # noqa: E402 + pv = live("paper_dvolspread") + + def metriche(rows: list[dict], key_m="net_modeled", key_r=None): + rm = np.array([float(r[key_m]) for r in rows]) + eq = np.cumprod(1 + rm) + pk = np.maximum.accumulate(eq) + out = dict(sh=float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0, + dd=float(np.max((pk - eq) / pk)), tot=float(np.prod(1 + rm) - 1)) + if key_r: + rr = np.array([float(r.get(key_r, np.nan)) for r in rows]) + rr = rr[np.isfinite(rr)] + out["tot_r"] = float(np.prod(1 + rr) - 1) if len(rr) else float("nan") + return out + + def coppia(name): + rec = RES[name]["rec"] + rep = {int(r["ts"]): r for r in RES[name]["rep"]} + keep = [r for r in rec if int(r["ts"]) in rep] + return keep, [rep[int(r["ts"])] for r in keep] + + print(f"\n {'gate':<26}{'metrica primaria':<26}{'soglia':>10}{'REGISTR.':>11}" + f"{'RICOSTR.':>11} verdetto") + + # --- STATARB ------------------------------------------------------------------------- + a, b = coppia("paper_statarb") + ma, mb = metriche(a, key_r="net_real"), metriche(b, key_r="net_real") + flip = (ma["sh"] >= GS.SH_DEPLOY) != (mb["sh"] >= GS.SH_DEPLOY) + print(f" {'STATARB ' + str(GS.DECISION):<26}{'Sharpe net_modeled':<26}" + f"{'>= ' + str(GS.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if flip else 'stesso'}") + print(f" {'':<26}{'maxDD':<26}{'< ' + f'{GS.DD_MAX:.0%}':>10}{ma['dd']:>10.1%}" + f"{mb['dd']:>11.1%} " + f"{'*** SI RIBALTA ***' if (ma['dd'] < GS.DD_MAX) != (mb['dd'] < GS.DD_MAX) else 'stesso'}") + hp_a = abs((1 + ma["tot"]) - (1 + ma["tot_r"])) * 100 + hp_b = abs((1 + mb["tot"]) - (1 + mb["tot_r"])) * 100 + print(f" {'':<26}{'fill-haircut cum (pp)':<26}{'< ' + str(GS.HAIRCUT_MAX_PP):>10}" + f"{hp_a:>11.2f}{hp_b:>11.2f} " + f"{'*** SI RIBALTA ***' if (hp_a < GS.HAIRCUT_MAX_PP) != (hp_b < GS.HAIRCUT_MAX_PP) else 'stesso'}") + + # --- XSR01 --------------------------------------------------------------------------- + a, b = coppia("paper_xsr") + K5 = "net_REAL-$5000" + ma, mb = metriche(a, key_r=K5), metriche(b, key_r=K5) + hc_a = (ma["tot"] - ma["tot_r"]) / abs(ma["tot"]) if ma["tot"] else float("nan") + hc_b = (mb["tot"] - mb["tot_r"]) / abs(mb["tot"]) if mb["tot"] else float("nan") + fl1 = (ma["sh"] >= GX.SH_DEPLOY) != (mb["sh"] >= GX.SH_DEPLOY) + fl2 = (hc_a <= GX.HAIRCUT_MAX) != (hc_b <= GX.HAIRCUT_MAX) + print(f" {'XSR01 ' + str(GX.DECISION):<26}{'Sharpe net_modeled':<26}" + f"{'>= ' + str(GX.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if fl1 else 'stesso'}") + print(f" {'':<26}{'haircut $5.000':<26}{'<= ' + f'{GX.HAIRCUT_MAX:.0%}':>10}" + f"{hc_a:>10.1%}{hc_b:>11.1%} {'*** SI RIBALTA ***' if fl2 else 'stesso'}") + + # --- DVOLSPREAD ---------------------------------------------------------------------- + a, b = coppia("paper_dvolspread") + ma, mb = metriche(a), metriche(b) + fl = (ma["sh"] < pv.KILL_SHARPE) != (mb["sh"] < pv.KILL_SHARPE) + print(f" {'DVOLSPREAD ' + pv.KILL_DATE:<26}{'Sharpe fwd (kill se sotto)':<26}" + f"{'> ' + f'{pv.KILL_SHARPE:+.2f}':>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} " + f"{'*** SI RIBALTA ***' if fl else 'stesso (KILL in entrambe)'}") + + print(""" + ⚠ QUESTA NON E' UNA LETTURA DEI GATE, e non deve diventarlo: le tre date sono nel futuro + (anticiparle sarebbe selezione-sul-forward, contro la regola del progetto) e con 28-54 barre + l'errore standard di uno Sharpe annualizzato e' 2,6-3,6. Serve a una cosa sola, e la mostra: + il difetto NON e' rumore centrato — sposta la metrica primaria abbastanza da attraversare la + soglia. Su STATARB la serie registrata dice +1,96 (sopra la soglia di deploy 0,5) e quella + ricostruita dice -2,02: **stesso codice, stessa finestra, verdetto opposto**. + ⚠ E c'e' un secondo segnale, gratis: uno Sharpe registrato di -15,8 su 28 barre e' IMPOSSIBILE + (|Sharpe| oltre 4 SE). Il gate `implausible_sharpe` esiste gia' in `altlib` dal 26/07 ma + nessuno lo punta sulle serie FORWARD — solo sui candidati. Un numero fuori scala e' un + rilevatore di guasti a costo zero, e in questo caso era li' da leggere da settimane.""") + + # ============================================================================================ # D — LA GUARDIA CHE MANCA A monitor_health # ============================================================================================ @@ -608,8 +701,10 @@ def sezione_D(): if v["parziale"] == "SI" and seg == "tace": miss.append(k) print(f" {k:<19}{c['sd_rec']:>9.2f}%{c['sd_rep']:>9.2f}%{rr:>10.3f} {seg}") - print(f" -> MANCA {len(miss)} monitor rotti su {sum(1 for v in RES.values() if v['parziale'] == 'SI')}" - f": {', '.join(miss) if miss else '-'}") + rotti = sum(1 for v in RES.values() if v["parziale"] == "SI") + print(f" -> segnala {rotti - len(miss)} dei {rotti} monitor che leggono una barra parziale,") + print(f" NON segnala il sano (`paper_combo`, 0.987), e MANCA {len(miss)}: " + f"{', '.join(miss) if miss else '-'}") print(" Motivo strutturale, non taratura: su una griglia oraria il difetto tocca 1 barra") print(" su 24 e sposta la vol dell'1-2%, cioe' DENTRO il rumore di stima di una vol su") print(" poche settimane. Abbassare la soglia per prenderlo genererebbe falsi allarmi ogni") @@ -697,17 +792,23 @@ def sezione_E(): ("paper_dvolspread", "DVOLSPREAD (kill)", pd.Timestamp("2026-10-24"), "returns.jsonl"), ("paper_dvolspread", "DVOLSPREAD (decisione)", pd.Timestamp("2027-01-24"), "returns.jsonl"), ] - print(f"\n {'monitor / gate':<34}{'barre oggi':>12}{'data gate':>13}{'se AZZERI':>12}" + print("\n IPOTESI PESSIMISTA — si ripara `advance()` e si fa `--reset` (la finestra riparte da") + print(" zero oggi). Con una barra al giorno, tornare a N barre richiede N giorni:") + print(f"\n {'monitor / gate':<42}{'barre':>7}{'data gate':>13}{'reset -> N barre':>18}" f"{'ritardo':>10}") + ritardi = [] for mon, nome, gate, ser in righe: n = len(read_jsonl(ROOT / "data" / mon / ser)) - # con una barra al giorno, tornare a n barre richiede n giorni dal reset rip = oggi + pd.Timedelta(days=n) rit = (rip - gate).days - print(f" {f'{mon} / {nome}':<34}{n:>12}{str(gate.date()):>13}" - f"{str(rip.date()):>12}{rit:>+9}g") - print("\n Cosi' letta, la riparazione sposta i tre gate di 1-2 mesi. MA la colonna giusta e'") - print(" un'altra, e cambia la decisione:") + ritardi.append((nome, rit)) + print(f" {f'{mon} / {nome}':<42}{n:>7}{str(gate.date()):>13}" + f"{str(rip.date()):>18}{rit:>+9}g") + peggiore = max(ritardi, key=lambda x: x[1]) + print(f"\n Anche nell'ipotesi pessimista il danno NON e' uniforme: l'unico gate che slitterebbe") + print(f" e' {peggiore[0]} ({peggiore[1]:+d} giorni) — gli altri hanno finestre cosi' giovani che") + print(" ricostruirebbero la propria numerosita' PRIMA della data di decisione. Ma la colonna") + print(" giusta e' ancora un'altra, e toglie di mezzo anche quel ritardo:") print(""" ✅ LA FINESTRA FORWARD **NON VA PERSA**: e' RICOSTRUIBILE, per tutti e sei i monitor. Le ore mancanti non sono mai state registrate, ma non e' li' che vivono: vivono nei feed @@ -719,10 +820,12 @@ def sezione_E(): minuti. LE DUE CONDIZIONI, ed e' giusto chiedere che siano verificate e non assunte: - (1) il feed non riscrive le barre gia' chiuse. **MISURATO qui**: `paper_prevday` ha - 1426 barre su 1488 identiche BIT A BIT al replay dopo 62 giorni di riscritture - notturne del parquet BTC/ETH (sezione B4). Per Hyperliquid la stessa cosa e' misurata - in `r0822_xsr_repro` T2 (Sharpe 2024 e 2025 riprodotti al centesimo su 731 barre). + (1) il feed non riscrive le barre gia' chiuse. **MISURATO qui, due volte e in modo + indipendente**: `paper_prevday` ha 1427 barre su 1488 identiche BIT A BIT al replay + dopo 62 notti di riscrittura del parquet BTC/ETH (B4); e le 6 barre che `paper_statarb` + recupero' durante l'incidente del feed coincidono anch'esse al bit (B2) — due percorsi + diversi, stesso verdetto. Per Hyperliquid la misura e' in `r0822_xsr_repro` T2 (Sharpe + 2024 e 2025 riprodotti al centesimo su 731 barre). (2) la config e' congelata e verificabile: `paper_dvolspread` lo controlla da solo (`frozen` nello stato, esce 1 se diverge); per gli altri il congelamento e' nel sorgente ed e' in git. @@ -743,11 +846,28 @@ def sezione_E(): verificabili in git (lo sono). Il costo vero e' (c), non i giorni: nessuna delle tre date di gate si sposta. - ⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, e va scritta nel diario: i libri REAL saltano i - ribilanci sotto il min-order, e quella decisione dipende dal capitale corrente, quindi il - percorso REAL rigenerato non e' il percorso REAL registrato — non e' "lo stesso numero - calcolato meglio", e' un secondo percorso deterministico. Per XSR01 conta: il gate del 23/10 - legge net_REAL-$5000 per l'haircut.""") + ⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, ma di poco — e il "di poco" e' MISURATO, non + assunto. I libri REAL saltano i ribilanci sotto il min-order e quella decisione dipende dal + capitale corrente: il percorso REAL rigenerato non e' "lo stesso numero calcolato meglio", e' + un secondo percorso deterministico. Contava sapere quanto, perche' il gate XSR01 del 23/10 + legge `net_REAL-$5000` per l'haircut: misurato in C2, **1,7% registrato contro 1,8% + ricostruito**. Trascurabile a $5.000 (i ticket superano il min-order comunque). Il caveat + resta vero in linea di principio e va scritto nel diario; non e' un ostacolo alla decisione. + + COSA FARE, MONITOR PER MONITOR (raccomandazione, non azione — questo script non tocca nulla). + ⚠ Le due cose sono SEPARATE: la RIPARAZIONE di `advance()` (fermarsi all'ultima barra chiusa) + ha senso su tutti e sei — su un monitor sano non cambia niente e lo protegge da un cambio di + griglia futuro; la RIGENERAZIONE della serie e' un'altra decisione, e va fatta solo dove il + difetto morde abbastanza da spostare cio' che si leggera': + paper_xsr rigenerare — 41 min/g, e il gate del 23/10 legge questa serie + paper_statarb rigenerare — 4 min/g, gate 27/09: e' il piu' vicino nel tempo + paper_dvolspread rigenerare — 2 min/g, il peggiore dei sei; kill-check il 24/10 + paper_prevday LASCIARE — 1.438/1.440 min/g: la rigenerazione cambierebbe lo 0,1% + della varianza e azzererebbe 1.488 barre di storia + paper_portfolio rigenerare — 19 min/g; nessun gate, ma e' la serie che il dashboard + mostra e che finisce nei diari come "il book sta facendo X" + paper_combo LASCIARE — sano; semmai va PROTETTO con un test che rompa se la sua + griglia smette di essere il calendario di borsa""") def main() -> None: @@ -763,6 +883,7 @@ def main() -> None: b_portfolio() b_combo() sezione_C() + sezione_C2() sezione_D() sezione_E() print("\n" + "=" * 102) diff --git a/scripts/research/r0822_prop_alloc.py b/scripts/research/r0822_prop_alloc.py index f226d64..6069fb7 100644 --- a/scripts/research/r0822_prop_alloc.py +++ b/scripts/research/r0822_prop_alloc.py @@ -362,11 +362,14 @@ def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.nda def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str, - ev_idx: np.ndarray, fu_idx: np.ndarray) -> tuple[float, float]: + ev_idx: np.ndarray, fu_idx: np.ndarray, + drift_mult: tuple = ()) -> tuple[float, float]: """Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore - standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno.""" - a = objective(wA, levA, firm, ev_idx, fu_idx)["jvec"] - b = objective(wB, levB, firm, ev_idx, fu_idx)["jvec"] + standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno. + ⚠ `drift_mult` va passato: dimenticarlo confronta due configurazioni NON stressate e + stampa un delta che non c'entra con la riga in cui compare (errore commesso in sessione).""" + a = objective(wA, levA, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"] + b = objective(wB, levB, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"] d = a - b return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d))) @@ -721,7 +724,8 @@ def main() -> None: w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) - dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, fu_idx) + dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx, + fu_idx, drift_mult=dmt) print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} " f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} " f"delta {dd_:+.3f} +-{sd_:.3f}") @@ -736,18 +740,24 @@ def main() -> None: print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente") print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'") print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:") - print(f"\n {'drift SKH01':>13} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " - f"{'delta appaiato':>16}") - for mult in (1.00, 0.66, 0.50, 0.33): - dmt = (("SKH01", mult),) + print(" Le ultime due righe sono il taglio CONGIUNTO su SKH01 E XS01 (i due sleeve che il") + print(" progetto misura come piu' fortunati / piu' recenti): e' il caso peggiore onesto.") + print(f"\n {'taglio drift':>21} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} " + f"{'delta appaiato':>17}") + for dmt in ((("SKH01", 1.00),), (("SKH01", 0.66),), (("SKH01", 0.50),), (("SKH01", 0.33),), + (("SKH01", 0.50), ("XS01", 0.50)), (("SKH01", 0.33), ("XS01", 0.00))): + mult = dict(dmt) rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for w in top_w for lv in LEVS] w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"]) lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"]) dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", - ev_idx, fu_idx) - print(f" {mult:>13.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " + ev_idx, fu_idx, drift_mult=dmt) + lab = " ".join(f"{k.replace('01','')} x{v:.2f}" for k, v in mult.items()) + assert abs(dd_ - (o_["J"] - oL["J"])) < 0.05, ( # guardia contro l'errore di sopra + f"delta appaiato {dd_:+.3f} incoerente con J {o_['J']:.3f} - {oL['J']:.3f}") + print(f" {lab:>21} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} " f"{dd_:>+11.3f} +-{sd_:.3f}") print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'") print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.")