diff --git a/CLAUDE.md b/CLAUDE.md index 399ae68..80ea817 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,9 +161,9 @@ sull'hold-out; cambiarne le soglie guardando l'esito, pure. | gate | data | criterio | stato | |---|---|---|---| -| **STATARB** | 2026-09-27 | soglie 24/07 (non toccate) + diagnostica statica sempre-short | 🚨 **si ribalta col difetto `advance()`**: riparata β†’ RITIRO, attuale β†’ candidato | +| **STATARB** | 2026-09-27 | soglie 24/07 (non toccate) + diagnostica statica sempre-short | βœ… monitor riparato e rigenerato il 26/08: la serie vera da' Sharpe **βˆ’1,61** a oggi β†’ coi criteri invariati punta al **RITIRO**. Si legge il 27/09, non prima | | **XSR01** | 2026-10-23 | Sharpe β‰₯1,0 **E** haircut a $5.000 ≀40% (se sfonda β†’ ritiro a prescindere), poi `weights_tilt_null` e capitale β‰₯$5k | 🚨 legge un monitor **mal tarato** (Β§5.3), e P(capitale β‰₯$5k quel giorno) = **0,0%** (mediana $2.635) β†’ **il criterio di capitale non e' raggiungibile**. Sotto la lente RENDITA il sleeve e' **eguagliato da un conto remunerato** (πŸ“Œ sotto) | -| **DVOLSPREAD** | kill 2026-10-24 se Sh<βˆ’0,50; decisione 2027-01-24 | (a) Sharpe>0 (b) marginale ADDS+robust_oos+insample_edge (c) **DSR β‰₯0,95** (d) `weights_tilt_null`; **veto** se barre attive <80% β†’ si estende, non si decide | il veto **non scatta** su una serie che registra 2 min/giorno (Β§5.1) | +| **DVOLSPREAD** | kill 2026-10-24 se Sh<βˆ’0,50; decisione 2027-01-24 | (a) Sharpe>0 (b) marginale ADDS+robust_oos+insample_edge (c) **DSR β‰₯0,95** (d) `weights_tilt_null`; **veto** se barre attive <80% β†’ si estende, non si decide | βœ… serie rigenerata il 26/08 (βˆ’4,41 a oggi, era βˆ’14,73 sulla serie rotta); il veto barre-attive ora conta barre VERE e la guardia PREMATURO sorveglia il monitor | | **GATE PROP-01** | chiuso 22-23/08 | (a) listino 13/13 βœ… (b) ancora SKH01 23/23 βœ… (c) economica+sopravvivenza 2/2 βœ… | **chiuso 3/3** β€” ma il numero operativo e' sceso 42% β†’ 7,8% β†’ 4,4% β†’ 2,6% | | **GATE PREVDAY-01** | scritto 23/08 | 10 condizioni | **8/10**: la cella che GIRA non e' quella che la selezione onesta sceglie, e quella viva **fallisce il DSR** (0,905) | | **`edge_watch`** (kill del book live) | continuo | **(A)** Sharpe rolling 36m < βˆ’0,5 β†’ si riapre `weights_tilt_null`+DSR, il book **non si spegne da solo**; **(B)** in un anno con DD buy&hold >10%, il DD di TP01 deve restare <75% di quello | stato 27/07: Sharpe 36m +1,51, protezione **8/8** | @@ -182,12 +182,16 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per ## 5. Debiti aperti β€” difetti noti NON riparati -1. 🚨 **`advance()` registra una frazione del giorno β†’ 4 monitor su 6 sono rotti.** - `fetch_hyperliquid`/`resample_tf` scrivono la barra del giorno **in corso**, `advance()` la consuma - e ci porta sopra `last_ts` β†’ `paper_statarb` **4 min/giorno**, `paper_dvolspread` 2, `paper_xsr` 41. - βœ… **La finestra non va persa** (misurato due volte: 0 barre chiuse cambiate su 1.761.259) β†’ l'azione - e' *"riparare **e rigenerare**"*, e **nessuna data di gate si sposta**. Guardia raccomandata e non - cablata: `ts_ultima_barra + cadenza ≀ mtime`, grazia 5 min. +1. βœ… **RIPARATO E RIGENERATO (2026-08-26).** `advance()` consumava la barra del giorno in corso: + ora tutti e 6 i monitor filtrano con `src/live/paper_guard.nuove_chiuse` (barra chiusa = + `ts + cadenza ≀ adesso`), le 4 serie rotte sono **rigenerate dallo stesso `start_ts`** + (`scripts/live/paper_regen.py`, vecchi file in `*.pre_regen_20260826.*`): statarb + **+1,95 β†’ βˆ’1,61**, dvolspread βˆ’14,73 β†’ βˆ’4,41, xsr βˆ’4,98 β†’ βˆ’2,72, prevday +0,39 β†’ +0,40. + **Nessuna data di gate si sposta.** La guardia e' cablata: `monitor_health` stato **PREMATURO** + (ultima barra che chiude dopo l'mtime, grazia 5 min; `open_labeled=False` per collect_chain). + `paper_portfolio` NON rigenerato (GTAA su ADJUSTED_LAST: replay β‰  serie registrata, P12) β€” + storia pre-fix dichiarata corrotta, coda non chiusa tolta. ⚠️ Nel fix, D6 pagata di nuovo: + `asi8` in pandas 3 e' in `us`, non `ns` β€” blindato con test su tre risoluzioni. 2. 🚨 **Il trasporto degli allarmi e' un punto singolo di guasto** β€” 6,9% di invii falliti (2/29). Fatte il 23/08: retry opzionale in `send()` e registrazione del motivo nel punto in cui veniva ingoiato. **Resta:** `alerted=True` marcato **prima** dell'invio β†’ un 🚨 perso e' perso per @@ -202,9 +206,10 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per `c-sexies` (33%) o `c-quater` (26%)? E il collaterale sconta il 2‰ al 31/12? (b) i payout di una prop firm: **ne' `c-sexies` ne' `c-quater`** β€” le fonti convergono su **lavoro autonomo**, nessuna prassi ufficiale. *Non sono pareri fiscali: sono domande da porre.* -5. ⚠️ **`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` va cancellato con nota, non - aggiustato:** misura `frac Β· n_asset` mentre con una chiave di scala la grandezza vera diventerebbe - `frac Β· n_asset Β· scala` β†’ **continua a passare e smette di controllare**. +5. βœ… **FATTO (2026-08-26):** `test_leva_massima_da_config...` cancellato con nota in + `tests/test_fee_sensitivity.py` β€” misurava `frac Β· n_asset`, con una chiave di scala avrebbe + continuato a passare smettendo di controllare. La guardia giusta e' il tetto sul PRODOTTO di + GATE SCALA-01, da cablare nel codice che leggera' la chiave quando esistera'. 6. ⚠️ **TLT ha 13,5 anni di storia in meno** (parte 2016-02 invece del 2002) β†’ **GTAA01 gira su CINQUE gambe prima del 2016**, e l'assente e' quella obbligazionaria. Non e' un fetch da rifare (IB ritorna 0 barre). β‡’ in-sample e hold-out di GTAA01 **non sono la stessa strategia**. @@ -217,12 +222,12 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per Deribit e distingue **manutenzione / venue giu' / NOSTRO gateway / non vedo**, e lo slot di release (**martedi' 09:00 UTC**) e' ora una costante dichiarata e testata. Resta scoperto il Rulebook vero e proprio: la sonda legge se il venue *risponde*, non cosa il venue *annuncia*. -9. ⚠️ **`tests/test_wave_0726.py::test_t1_canonical_riproduce_il_backtest_ufficiale` FALLISCE** - (dal ~24-25/08): Sharpe hold-out SKH01 `canonical` **1,9223** contro la banda cablata - `1.3 < h < 1.9`. **Il codice non e' cambiato, sono cambiati i DATI** β€” `data/raw/` e' gitignored - e il cron lo ricostruisce ogni notte, quindi la finestra si allunga e il numero deriva (verso - l'**alto**). **La banda non e' stata allargata**: si guarda sotto prima di toccarla (lezione - 07/08). Suite: **730 passati, 1 fallito** (25/08). +9. βœ… **INDAGATO E RIPARATO (2026-08-26, `r0826_skh_band_drift.py`).** Il dato regge (taglio + al 02/07 riproduce l'audit: 1,6376; in-sample 1,424 identico su ogni taglio); la deriva era la + finestra hold-out che si allunga β€” e πŸ“Œ **la SOLA settimana 15-22/08 vale +0,35 di Sharpe + hold-out** (rally ETH con SKH01 long): il numero e' fragile, coerente con Β§2. Il test ora + **taglia il feed al 02/07** e verifica la riproduzione stretta (Β±0,02): si rompe solo se + cambiano codice o dato sulla finestra congelata. Suite: **751 passati, 0 falliti** (26/08). 10. ⚠️ **Nessuna tabella pubblicata prima del 22/08 contiene fisco E funding insieme.** L'unica congiunta e' quella in `30-piano-capitale-fisco.md` β€” le altre vanno lette con lo sconto. 11. 🚨 **Le credenziali Deribit esistono SOLO dentro il gateway** (`cerbero-mcp`): in locale c'e' @@ -238,9 +243,11 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per da **$334 a $2.500/asset β€” ~7,5x di leva** su un conto da $668, per giunta sul ramo `eq_fallback` che **allerta e NON blocca**. βœ… Riparato il 2026-08-25 con una fixture **autouse** in `tests/conftest.py` (strutturale: non si chiede a ogni autore di ricordarsene β€” quella scommessa - ha gia' perso il 26/07 e il 21/08) + guardia in `test_cap_watermark.py`. **Resta il principio: - un test non deve poter scrivere in `data/live/`; oggi e' deviato solo il watermark, non - `trades.db` ne' `book_executions.jsonl`.** + ha gia' perso il 26/07 e il 21/08) + guardia in `test_cap_watermark.py`. βœ… **Esteso il + 2026-08-26:** deviati anche `trades.db` (wrapper su `connect` β€” il default e' catturato alla + definizione, patchare la costante non bastava) e `docs/journal/`; per `book_executions.jsonl` + (caricato ad-hoc via importlib, irraggiungibile da conftest) impronta a inizio suite e verifica + a fine suite, col falso positivo possibile (fill reale del cron :47) dichiarato nel messaggio. --- @@ -457,7 +464,7 @@ uv run python scripts/live/trades_db.py --report # stato libro d uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill uv run python scripts/live/journal.py # voce del giorno (numeri + lettura) uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare -uv run pytest # test (731: 730 ok, 1 noto Β§5.9) +uv run pytest # test (751, tutti verdi dal 26/08) ``` ```python diff --git a/docs/diary/2026-08-26-advance-riparato-e-debiti.md b/docs/diary/2026-08-26-advance-riparato-e-debiti.md new file mode 100644 index 0000000..32ff1f3 --- /dev/null +++ b/docs/diary/2026-08-26-advance-riparato-e-debiti.md @@ -0,0 +1,95 @@ +# 2026-08-26 β€” `advance()` riparato e rigenerato, quattro debiti chiusi in un giorno + +Quattro debiti di CLAUDE.md Β§5 chiusi: **Β§5.1** (advance + rigenerazione + guardia), +**Β§5.12** (isolamento test esteso), **Β§5.5** (test di leva cancellato con nota), +**Β§5.9** (test rosso indagato e ancorato). Suite: **751 passati, 0 falliti** β€” tutta verde +per la prima volta dal ~24/08. + +## Β§5.1 β€” advance() consuma solo barre CHIUSE, e le serie sono RIGENERATE + +**Il fix** (`src/live/paper_guard.py` + 6 monitor): la riga +`new = [i for i in range(len(ts)) if ts[i] > last_ts]` diventa `PG.nuove_chiuse(ts, last_ts, +cadenza)` β€” una barra open-labeled e' chiusa quando `ts + cadenza <= adesso`. Filtro condiviso, +importato da tutti e sei i monitor (anche `paper_combo`, che era sano *per caso*: ora e' sano +*per costruzione*). + +⚠️ **D6 pagata DI NUOVO scrivendo il fix:** la prima stesura di `indice_chiuso` usava `asi8` +assumendo nanosecondi β€” pandas 3 usa `us` di default, e l'intero nella scala sbagliata dava +"chiusa" la barra del giorno in corso **senza eccezioni**. Beccata dal test di fumo, riparata +con la sottrazione dall'epoca (la forma di `resample_tf`), e blindata da un test che prova +`ns/us/s`. *La regola che stavo cablando e' quella che stavo violando.* + +**La guardia** (era "raccomandata e non cablata"): `monitor_health` ha ora lo stato +**PREMATURO** β€” ultima barra che chiude DOPO l'mtime del file, grazia 5 min. Derivata dalle +spec esistenti (P1), con `open_labeled=False` per `collect_chain` che timbra i giri, non barre +(P14: senza il flag avrebbe allertato sempre). Controllo positivo sul dato vivo: **ha segnalato +esattamente i 5 rotti e taciuto sui 2 sani** prima della rigenerazione; dopo, 7/7 OK. + +**La rigenerazione** (`scripts/live/paper_regen.py`): stesso `start_ts` pre-registrato, config +congelata, replay del codice di produzione riparato β€” il metodo validato due volte dall'audit +del 22/08 (0 barre chiuse cambiate su 1.427/1.488 + 6/6 al bit). Vecchi file archiviati come +`*.pre_regen_20260826.*` (evidenza del difetto, nel perimetro di backup). **Nessuna data di +gate si sposta.** + +| monitor | barre | Sharpe rotto β†’ vero | +|---|---|---| +| `paper_statarb` (gate **27/09**) | 58 β†’ 57 | **+1,95 β†’ βˆ’1,61** β€” il ribaltamento previsto dall'audit | +| `paper_dvolspread` (kill 24/10) | 32 β†’ 31 | βˆ’14,73 β†’ βˆ’4,41 | +| `paper_xsr` (gate 23/10) | 32 β†’ 31 | βˆ’4,98 β†’ βˆ’2,72 | +| `paper_prevday` | 1584 β†’ 1584 | +0,39 β†’ +0,40 | + +`paper_portfolio` **non** rigenerato (gamba GTAA su ADJUSTED_LAST ri-aggiustato: il replay non +sarebbe la serie registrata β€” P12); tolta la sola coda non chiusa, cosi' la guardia non resta +accesa per sempre su un difetto gia' riparato (P14). Storia pre-fix dichiarata corrotta. + +## Β§5.12 β€” un test non puo' piu' scrivere nel libro di bordo vivo + +`tests/conftest.py`: oltre al watermark, ora deviati **`trades.db`** (wrapper su `connect` β€” +il default e' catturato alla definizione, patchare la costante non serviva a niente) e +**`docs/journal/`** (pagine tracciate da git). Per `book_executions.jsonl`, che i test caricano +ad-hoc con importlib e conftest non puo' raggiungere prima: **impronta a inizio suite, verifica +a fine suite**, col falso positivo possibile (fill reale del cron :47 durante la suite) +dichiarato nel messaggio. + +## Β§5.5 β€” il test di leva e' CANCELLATO, non aggiustato + +`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` misurava `frac x n_asset`: con una +chiave di scala la grandezza vera diventerebbe `frac x n_asset x scala` e il test avrebbe +continuato a passare smettendo di controllare. Al suo posto una nota che rimanda al tetto sul +PRODOTTO di GATE SCALA-01, da cablare nel codice che leggera' la chiave, quando esistera'. + +## Β§5.9 β€” il test rosso non segnalava un guasto: segnalava una banda scritta male + +`r0826_skh_band_drift.py` β€” feed tagliato a date crescenti, stesso codice di produzione: + +| taglio | Sharpe hold-out | +|---|---| +| 02/07 (audit) | **1,6376** β€” riproduce l'audit | +| 25/07 (banda cablata) | 1,5751 | +| 01β†’15/08 | 1,566 β†’ 1,547 (scivola piano) | +| **22/08** | **1,8966** | +| oggi | 1,9306 β€” riproduce il rosso | + +**Il dato regge** (in-sample 1,424 **identico su ogni taglio**; il taglio all'epoca riproduce +l'epoca). Il codice non c'entra. πŸ“Œ **La scoperta vera: la SOLA settimana 15-22/08 (rally ETH +con SKH01 long) vale +0,35 di Sharpe hold-out** β€” il numero e' cosi' fragile, e chi cita +"hold-out 1,9" oggi citerebbe 1,55 di una settimana fa. Coerente con Β§2: l'hold-out canonico +di SKH01 era gia' il numero da non citare. + +**La riparazione:** il test ora **taglia il feed al 02/07** e verifica la riproduzione stretta +(Β±0,02) di full/in-sample/hold-out/maxDD. Si rompe se cambiano codice o dato sulla finestra +congelata β€” e per nessun altro motivo. La deriva del vivo non e' affare suo: per quella ci +sono i monitor (e da oggi la guardia PREMATURO). + +Nel farlo, un altro inciampo istruttivo: la prima esecuzione del drift stampava **sette righe +identiche** β€” `run_asset` memoizza in un dict di modulo (`_CACHE`), non in `lru_cache`, e lo +svuotamento non lo toccava. Anche il test riparato ora pulisce quel dict, prima E dopo, per +non avvelenare gli altri test. + +## Conseguenze sui gate (nessuna data si muove) + +- **STATARB 27/09**: il monitor ora misura la serie vera (**βˆ’1,61** a oggi). Coi criteri + pre-registrati invariati, a oggi punta al RITIRO β€” si legge quel giorno, non prima. +- **XSR01 23/10 / DVOLSPREAD 24/10**: metrica primaria finalmente su serie reali. + Restano i difetti dichiarati (Β§5.3 pavimento del gate XSR01; veto DVOLSPREAD ora sorvegliato + dalla guardia). diff --git a/docs/memory/40-produzione-e-deploy.md b/docs/memory/40-produzione-e-deploy.md index d7be6e8..1c279b6 100644 --- a/docs/memory/40-produzione-e-deploy.md +++ b/docs/memory/40-produzione-e-deploy.md @@ -790,6 +790,26 @@ SORGENTE** di `cron_daily.sh` β€” ogni chiamata a `journal.py` deve avere `--gio perche' `journal.py` **senza argomenti scrive OGGI**. La guardia **non vieta** di rigenerare spesso: vieta di scrivere la pagina **una volta e lasciarla li'**. +🚨 **RIPARATO E RIGENERATO (2026-08-26): `advance()` e i 6 forward-monitor (Β§5.1).** +Il fix e' un filtro condiviso (`src/live/paper_guard.py`): barra open-labeled chiusa = +`ts + cadenza ≀ adesso`, importato da tutti e sei i monitor β€” `paper_combo` compreso, che era +sano *per caso* (aspettava la chiusura di una borsa, non per progetto) ed e' ora sano *per +costruzione*. Rigenerazione con `scripts/live/paper_regen.py`: stesso `start_ts` pre-registrato, +config congelata, replay del codice di produzione riparato (il metodo validato 2 volte +dall'audit 22/08); evidenza del difetto archiviata in `*.pre_regen_20260826.*` dentro il +perimetro di backup. Esiti (Sharpe rotto β†’ vero): 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 ++0,39 β†’ +0,40. `paper_portfolio` non rigenerato (GTAA legge ADJUSTED_LAST ri-aggiustato: il +replay non sarebbe la serie registrata, P12): tolta solo la coda non chiusa, storia pre-fix +dichiarata corrotta. La guardia raccomandata dall'audit e' **cablata in `monitor_health`**: +stato **PREMATURO** (l'ultima barra chiude dopo l'mtime del file, grazia 5 min), derivato +dalle spec esistenti (P1), con `open_labeled=False` per `collect_chain` che timbra giri e non +barre (P14). Controllo positivo sul dato vivo: prima della rigenerazione segnala **esattamente +i 5 rotti** e tace sui 2 sani; dopo, 7/7 OK. ⚠️ Lezione nel fix: la prima stesura di +`indice_chiuso` usava `asi8` assumendo `ns` β€” pandas 3 usa `us`, e la barra del giorno in corso +risultava "chiusa" **senza eccezioni** (D6, pagata di nuovo scrivendo il codice che doveva +impedirla). Blindato con un test su tre risoluzioni (`ns/us/s`). + 🚨 **RIPARATO (2026-08-26): il P&L di giornale contava i VERSAMENTI come profitto.** Il Β«giornoΒ» era un delta di equity fra letture: la voce del 25/08 dichiarava **+$1.414,57 di P&L** quando $1.399,39 erano il deposito USDC dell'operatore, e il Β«cumulato dall'armingΒ» diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index d621e69..78edd62 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -631,6 +631,11 @@ 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.** +βœ… **CHIUSO IL 2026-08-26:** `advance()` riparato in tutti e 6 i monitor +(`src/live/paper_guard.py`), serie rigenerate dallo stesso `start_ts` +(`scripts/live/paper_regen.py`; statarb +1,95 β†’ **βˆ’1,61**), guardia PREMATURO cablata in +`monitor_health` (5/6 segnalati prima della rigenerazione, 7/7 OK dopo). Dettaglio nel diario +`2026-08-26-advance-riparato-e-debiti.md`. βœ… **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 diff --git a/scripts/live/paper_combo.py b/scripts/live/paper_combo.py index 05b8e53..81281bd 100644 --- a/scripts/live/paper_combo.py +++ b/scripts/live/paper_combo.py @@ -18,6 +18,7 @@ sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np, pandas as pd from src.portfolio.sleeves import _tp01_returns, _tp01_positions from src.portfolio.gtaa import gtaa_returns, gtaa_weights +from src.live import paper_guard as PG STATE_DIR = PROJECT_ROOT / "data" / "paper_combo" STATE = STATE_DIR / "state.json" @@ -77,6 +78,10 @@ def advance(): return st last = pd.Timestamp(st["last"]) nn = naked[naked.index > last]; gg = guard[guard.index > last] + # SOLO giorni CHIUSI. L'audit 22/08 ha trovato questo monitor sano PER CASO (aspetta la + # chiusura di una borsa, non per progetto): il filtro lo rende sano PER COSTRUZIONE. + m = PG.indice_chiuso(nn.index, PG.MS_1D) + nn, gg = nn[m], gg[PG.indice_chiuso(gg.index, PG.MS_1D)] if len(nn): e = st["equity"]; pk = st["peak"]; dd = st["max_dd"] eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = [] diff --git a/scripts/live/paper_dvolspread.py b/scripts/live/paper_dvolspread.py index 0f997cd..4e89897 100644 --- a/scripts/live/paper_dvolspread.py +++ b/scripts/live/paper_dvolspread.py @@ -85,6 +85,7 @@ 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) @@ -152,7 +153,9 @@ def init_state(P: dict) -> dict: def advance(st: dict, P: dict) -> dict: ts, dt = P["ts"], P["dt"] - new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]] + # 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) diff --git a/scripts/live/paper_portfolio.py b/scripts/live/paper_portfolio.py index 5a9be6e..a1cbd03 100644 --- a/scripts/live/paper_portfolio.py +++ b/scripts/live/paper_portfolio.py @@ -17,6 +17,7 @@ sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np, pandas as pd from src.portfolio.portfolio import StrategyPortfolio from src.portfolio.sleeves import active_sleeves +from src.live import paper_guard as PG STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio" STATE = STATE_DIR / "state.json" @@ -51,6 +52,10 @@ def advance(): return st last = pd.Timestamp(st["last"]) new = r[r.index > last] + # SOLO giorni CHIUSI: l'ultima riga dei rendimenti giornalieri e' il giorno in corso + # (~19 min/giorno registrati, r0822_monitor_audit). E' un monitor da dashboard, ma la + # regola D2 vale anche qui. + new = new[PG.indice_chiuso(new.index, PG.MS_1D)] if len(new): eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"] lines = [] diff --git a/scripts/live/paper_prevday.py b/scripts/live/paper_prevday.py index 4ca338a..3b5be84 100644 --- a/scripts/live/paper_prevday.py +++ b/scripts/live/paper_prevday.py @@ -38,6 +38,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from src.backtest.harness import load # noqa: E402 +from src.live import paper_guard as PG # noqa: E402 from src.strategies.prevday_breakout import target as prevday_target # noqa: E402 from src.strategies import prevday_breakout as pb # noqa: E402 @@ -93,7 +94,10 @@ def advance(st: dict, dfs: dict) -> dict: dt=pd.to_datetime(df["datetime"]).values, r=r, tgt=prevday_target(df)) common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"])) - new_ts = [t for t in common if t > st["last_ts"]] + # SOLO barre 1h CHIUSE: il parquet 1h puo' contenere l'ora in corso (1 barra su 24 β€” + # r0822_monitor_audit); la si lascia al giro successivo + new_ts = [t for t in common + if t > st["last_ts"] and PG.chiusa(int(t), PG.MS_1H)] if not new_ts: return st idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS} diff --git a/scripts/live/paper_regen.py b/scripts/live/paper_regen.py new file mode 100644 index 0000000..3d5667f --- /dev/null +++ b/scripts/live/paper_regen.py @@ -0,0 +1,228 @@ +"""paper_regen.py β€” RIGENERA le serie dei forward-monitor rotti da `advance()` (Β§5.1). + +PERCHE' RIGENERARE E NON AZZERARE. L'audit r0822_monitor_audit ha provato due volte, in modo +indipendente, che il replay sul pannello di oggi E' la serie corretta sulla stessa finestra: +(a) `paper_prevday` ha 1427/1488 barre identiche al bit dopo 62 notti di riscrittura del feed; +(b) le 6 barre chiuse di `paper_statarb` recuperate dopo il guasto EPERM coincidono al bit. +Quindi la finestra forward NON va persa: si rigenera dallo STESSO `start_ts` pre-registrato, +con la config congelata e il codice di produzione riparato β€” e nessuna data di gate si sposta. + +COSA FA, per ciascun monitor (statarb Β· dvolspread Β· xsr Β· prevday): + 1. archivia i file correnti come *.pre_regen_.* (NON li cancella: sono l'evidenza + del difetto, e stanno nel perimetro di backup); + 2. ricostruisce lo stato iniziale all'ORIGINALE `start_ts` (stessa costruzione dell'audit); + 3. riesegue `advance()` di produzione β€” che ora consuma SOLO barre chiuse (paper_guard); + 4. salva lo stato e stampa il verdetto a runtime (N11): barre, Sharpe vecchio vs nuovo, + e la prova che l'ultima barra registrata e' chiusa. + +`paper_portfolio` e `paper_combo` NON si rigenerano qui, e la scelta e' dichiarata: la gamba +GTAA legge `ADJUSTED_LAST` di IB ri-aggiustato all'indietro, quindi il replay NON sarebbe la +serie che un monitor sano avrebbe registrato (P12: meglio una storia corrotta e dichiarata di +una riscritta e spacciata per registrata). Sono dashboard senza gate; dal fix in poi scrivono +solo barre chiuse. + + uv run python scripts/live/paper_regen.py # tutti e 4 + uv run python scripts/live/paper_regen.py --solo paper_statarb +""" +from __future__ import annotations + +import importlib.util +import json +import shutil +import sys +from datetime import datetime, timezone +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +ANN = float(np.sqrt(365.0)) +SUFFISSO = datetime.now(timezone.utc).strftime("pre_regen_%Y%m%d") + + +def live(name: str): + spec = importlib.util.spec_from_file_location(f"_regen_{name}", + ROOT / "scripts" / "live" / f"{name}.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +def sharpe_jsonl(path: Path, campo: str = "net_modeled") -> tuple[float, int]: + if not path.exists(): + return float("nan"), 0 + r = [] + for ln in path.read_text().splitlines(): + ln = ln.strip() + if ln: + r.append(float(json.loads(ln)[campo])) + r = np.asarray(r) + if len(r) < 2 or r.std() == 0: + return float("nan"), len(r) + return float(r.mean() / r.std() * ANN), len(r) + + +def archivia(d: Path, nomi: tuple[str, ...]) -> None: + for n in nomi: + f = d / n + if f.exists(): + dest = d / f"{f.stem}.{SUFFISSO}{f.suffix}" + if dest.exists(): + raise SystemExit(f" {dest} esiste gia': rigenerazione doppia nello stesso " + "giorno? Verificare a mano prima di riprovare.") + shutil.move(str(f), str(dest)) + print(f" archiviato {f.name} -> {dest.name}") + + +def verdetto(nome: str, d: Path, mod, st: dict, sh_prima: float, n_prima: int, + cadenza_ms: int) -> None: + from src.live.paper_guard import chiusa + sh_dopo, n_dopo = sharpe_jsonl(d / "returns.jsonl") + ult = st["last_ts"] if "last_ts" in st else None + ok_chiusa = chiusa(int(ult), cadenza_ms) if ult else False + print(f" barre: {n_prima} registrate (rotte) -> {n_dopo} rigenerate (chiuse)") + print(f" Sharpe ann. net_modeled: {sh_prima:+.2f} (serie rotta) -> {sh_dopo:+.2f} " + f"(serie vera)") + print(f" ultima barra {pd.Timestamp(int(ult), unit='ms', tz='UTC').date() if ult else '?'} " + f"chiusa: {'SI' if ok_chiusa else 'NO β€” QUALCOSA NON VA'}") + if not ok_chiusa: + raise SystemExit(f" {nome}: la rigenerazione ha consumato una barra NON chiusa") + + +def regen_statarb() -> None: + print("\n== paper_statarb (gate 2026-09-27) " + "=" * 50) + d = ROOT / "data" / "paper_statarb" + ps = live("paper_statarb") + st_old = json.loads((d / "state.json").read_text()) + sh0, n0 = sharpe_jsonl(d / "returns.jsonl") + j = ps.build_joint("1d") + ts, _, pos, _ = ps._signal(j) + i0 = int(np.where(ts == st_old["start_ts"])[0][0]) + archivia(d, ("returns.jsonl", "trades.jsonl", "state.json")) + st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0, + pos_modeled=float(pos[i0]), pos_real=float(pos[i0]), + 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) + st = ps.advance(st, j) + ps._state_io(st) + verdetto("paper_statarb", d, ps, st, sh0, n0, 86_400_000) + + +def regen_dvolspread() -> None: + print("\n== paper_dvolspread (kill 2026-10-24 / decisione 2027-01-24) " + "=" * 24) + d = ROOT / "data" / "paper_dvolspread" + pv = live("paper_dvolspread") + st_old = json.loads((d / "state.json").read_text()) + sh0, n0 = sharpe_jsonl(d / "returns.jsonl") + P = pv._panel() + i0 = int(np.where(P["ts"] == st_old["start_ts"])[0][0]) + archivia(d, ("returns.jsonl", "state.json")) + st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0, n_active=0, + n_flat_signal=0, n_flat_nodata=0, + wb_modeled=float(P["wb"][i0]), wb_real=float(P["wb"][i0]), + 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) + st = pv.advance(st, P) + pv._state_io(st) + verdetto("paper_dvolspread", d, pv, st, sh0, n0, 86_400_000) + + +def regen_xsr() -> None: + print("\n== paper_xsr (gate 2026-10-23) " + "=" * 54) + d = ROOT / "data" / "paper_xsr" + px = live("paper_xsr") + st_old = json.loads((d / "state.json").read_text()) + sh0, n0 = sharpe_jsonl(d / "returns.jsonl") + archivia(d, ("returns.jsonl", "state.json")) + st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0, + syms=st_old["syms"], + modeled=px._book(px.MODELED_CAPITAL, len(st_old["syms"])), + reals={n: px._book(c, len(st_old["syms"])) for c, n in px.REAL_BOOKS}) + st = px.advance(st) + px._state_io(st) + verdetto("paper_xsr", d, px, st, sh0, n0, 86_400_000) + + +def regen_prevday() -> None: + print("\n== paper_prevday (griglia ORARIA, nessun gate a data fissa) " + "=" * 24) + d = ROOT / "data" / "paper_prevday" + pp = live("paper_prevday") + st_old = json.loads((d / "state.json").read_text()) + sh0, n0 = sharpe_jsonl(d / "returns.jsonl") + dfs = pp.build_bars() + S = st_old["start_ts"] + pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS} + archivia(d, ("returns.jsonl", "trades.jsonl", "state.json")) + st = 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) + st = pp.advance(st, dfs) + pp._state_io(st) + verdetto("paper_prevday", d, pp, st, sh0, n0, 3_600_000) + + +def trim_portfolio() -> None: + """`paper_portfolio` NON si rigenera (la gamba GTAA legge ADJUSTED_LAST ri-aggiustato: + il replay non sarebbe la serie registrata β€” P12). Ma la sua CODA contiene la riga del + giorno in corso, scritta dal codice vecchio: senza toglierla la guardia PREMATURO + resterebbe accesa per sempre su un difetto gia' riparato (P14). Il taglio e' onesto: + la storia registrata resta com'e' (troncata e DICHIARATA in memoria), va via solo la + riga che dice di essere un giorno e non lo e' β€” la versione chiusa la riscrive il + prossimo giro del cron col codice riparato.""" + from src.live.paper_guard import MS_1D, indice_chiuso + print("\n== paper_portfolio (dashboard β€” TRIM della coda non chiusa, non rigenerazione) ==") + d = ROOT / "data" / "paper_portfolio" + righe = (d / "equity.csv").read_text().strip().splitlines() + testa, dati = righe[0], righe[1:] + date = pd.DatetimeIndex([r.split(",")[0] for r in dati]) + m = indice_chiuso(date, MS_1D) + tolte = [r for r, k in zip(dati, m) if not k] + if not tolte: + print(" coda gia' pulita: niente da fare") + return + archivia(d, ("equity.csv", "state.json")) + dati_ok = [r for r, k in zip(dati, m) if k] + (d / "equity.csv").write_text("\n".join([testa] + dati_ok) + "\n") + eq = [float(r.split(",")[1]) for r in dati_ok] + peak, dd = 0.0, 0.0 + for e in eq: + peak = max(peak, e) + dd = max(dd, (peak - e) / peak if peak > 0 else 0.0) + st_old = json.loads((d / f"state.{SUFFISSO}.json").read_text()) + st = dict(st_old, last=dati_ok[-1].split(",")[0], equity=eq[-1], peak=peak, max_dd=dd, + n_days=st_old["n_days"] - len(tolte)) + (d / "state.json").write_text(json.dumps(st, indent=2)) + for r in tolte: + print(f" tolta riga non chiusa: {r}") + print(f" ultima riga ora: {dati_ok[-1]} (peak/max_dd ricalcolati dalla serie)") + + +REGEN = dict(paper_statarb=regen_statarb, paper_dvolspread=regen_dvolspread, + paper_xsr=regen_xsr, paper_prevday=regen_prevday, + paper_portfolio=trim_portfolio) + + +def main() -> None: + print("=" * 90) + print(" RIGENERAZIONE forward-monitor β€” stesso start_ts, config congelata, solo barre chiuse") + print("=" * 90) + solo = None + if "--solo" in sys.argv: + solo = sys.argv[sys.argv.index("--solo") + 1] + if solo not in REGEN: + raise SystemExit(f"monitor sconosciuto: {solo} (validi: {', '.join(REGEN)})") + for nome, fn in REGEN.items(): + if solo and nome != solo: + continue + fn() + print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO) + + +if __name__ == "__main__": + main() diff --git a/scripts/live/paper_statarb.py b/scripts/live/paper_statarb.py index 109a2ad..96dd5e0 100644 --- a/scripts/live/paper_statarb.py +++ b/scripts/live/paper_statarb.py @@ -51,6 +51,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) # Segnale ESATTO dello sweep (nessuna reimplementazione β†’ identitΓ  garantita col backtest). from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402 +from src.live import paper_guard as PG # noqa: E402 STATE_DIR = PROJECT_ROOT / "data" / "paper_statarb" STATE_FILE = STATE_DIR / "state.json" @@ -99,7 +100,9 @@ def init_state(j: pd.DataFrame) -> dict: def advance(st: dict, j: pd.DataFrame) -> dict: ts, dt, pos, sr = _signal(j) - new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]] + # SOLO barre CHIUSE: `build_joint('1d')` passa da `resample_tf`, che NON scarta il giorno + # in corso (1 barra 1h su 24) β€” consumarlo faceva registrare ~4 min/giorno (r0822_monitor_audit) + new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D) if not new: return st pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente) diff --git a/scripts/live/paper_xsr.py b/scripts/live/paper_xsr.py index f524b61..b03b908 100644 --- a/scripts/live/paper_xsr.py +++ b/scripts/live/paper_xsr.py @@ -65,6 +65,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research")) # Segnale ESATTO dello studio (nessuna reimplementazione -> niente drift col backtest). from r0725_statarb_multi import BASE, MIN_BARS, load_hl, signal, universe # noqa: E402 +from src.live import paper_guard as PG # noqa: E402 STATE_DIR = PROJECT_ROOT / "data" / "paper_xsr" STATE_FILE = STATE_DIR / "state.json" @@ -158,7 +159,9 @@ def advance(st: dict) -> dict: print(f" [XSR01] universo cambiato ({len(st['syms'])} -> {len(syms)}): " "la finestra forward richiede universo costante. Usa --reset per ripartire.") return st - new = [i for i in range(len(ts)) if ts[i] > st["last_ts"]] + # SOLO barre CHIUSE: l'ultima riga del parquet HL e' il giorno IN CORSO, e consumarla + # e' il difetto che faceva registrare ~41 minuti di mercato al giorno (r0822_monitor_audit) + new = PG.nuove_chiuse(ts, st["last_ts"], PG.MS_1D) if not new: return st for i in new: diff --git a/scripts/research/r0826_skh_band_drift.py b/scripts/research/r0826_skh_band_drift.py new file mode 100644 index 0000000..2d87eb0 --- /dev/null +++ b/scripts/research/r0826_skh_band_drift.py @@ -0,0 +1,111 @@ +"""r0826_skh_band_drift.py β€” si guarda SOTTO il test rosso Β§5.9 prima di toccare la banda. + +IL FATTO. `test_t1_canonical_riproduce_il_backtest_ufficiale` fallisce dal ~24-25/08: +Sharpe hold-out SKH01 `canonical` = 1,9223 contro la banda cablata `1.3 < h < 1.9` +(scritta il 25/07 sull'audit del 02/07: ~1,64). Il codice non e' cambiato; `data/raw/` +e' gitignored e il cron lo ricostruisce ogni notte -> la finestra si allunga e il numero +deriva. Lezione 07/08: un test che fallisce senza che il codice sia cambiato sta +segnalando QUALCOSA β€” si misura cosa, non si allarga la banda a occhio. + +LA MISURA. Stesso codice di produzione (`r0726_skh_onbook.skh_series`), stesso `sh3`, +ma col feed TAGLIATO a date crescenti: se il numero alla data dell'audit riproduce +~1,64 e la curva sale in modo regolare con le barre aggiunte, la deriva e' CRESCITA +DELLA FINESTRA (hold-out 2025+ che si allunga con settimane favorevoli), non un dato +corrotto ne' un codice cambiato. Se invece il taglio all'epoca NON riproduce il numero +dell'epoca, il problema e' nel DATO RICOSTRUITO e la banda e' l'ultima delle questioni. + +COSA MI ASPETTO PRIMA DI MISURARE (M12): riproduzione al taglio 02/07 dentro Β±0,05; +curva monotona o quasi verso 1,92 (ago 2026 e' stato un rally forte, BTC +21%/30g, +e SKH01 era long ETH da meta' agosto). + + uv run python scripts/research/r0826_skh_band_drift.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(p)) + +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0726_skh_onbook as ob # noqa: E402 + +TAGLI = ("2026-07-02", # la data dell'audit che ha scritto ~1,64 + "2026-07-25", # il giorno in cui la banda e' stata cablata nel test + "2026-08-01", "2026-08-08", "2026-08-15", "2026-08-22", + None) # oggi: deve riprodurre l'1,9223 del test rosso + +_GET5M_VERO = r02.get5m.__wrapped__ # la funzione nuda, senza lru_cache + + +def _taglia(cut_ms: int | None): + """Rimpiazza get5m con una versione tagliata e svuota TUTTE le cache a valle.""" + if cut_ms is None: + def g(asset: str): + return _GET5M_VERO(asset) + else: + def g(asset: str): + df = _GET5M_VERO(asset) + return df[df["timestamp"] < cut_ms].reset_index(drop=True) + r02.get5m = g + # ⚠️ run_asset non usa lru_cache: memoizza in un DICT di modulo (`_CACHE`) β€” svuotare + # solo le lru_cache serviva a niente e la prima stesura di questo script ha misurato + # SETTE volte la stessa finestra (tutte le righe identiche). Si svuota il dict. + r02._CACHE.clear() + r02.skh_port.cache_clear() + + +def main() -> None: + print("=" * 96) + print(" SKH01 canonical β€” Sharpe (full / in-sample / HOLD-OUT) in funzione della finestra dati") + print(" hold-out = barre >= 2025-01-01: si ALLUNGA ogni notte col cron") + print("=" * 96) + print(f" {'taglio':>12} {'barre HO (g)':>13} {'full':>7} {'in-s':>7} {'HOLD-OUT':>9} " + f"{'maxDD':>8} nota") + print(" " + "-" * 90) + esiti = {} + for cut in TAGLI: + cut_ms = None if cut is None else int(pd.Timestamp(cut, tz="UTC").value // 10**6) + _taglia(cut_ms) + s, _ = ob.skh_series(0, "canonical") + full, is_, hold, dd = ob.sh3(s) + n_ho = int((s.index >= r02.HOLDOUT).sum()) + nota = "" + if cut == "2026-07-02": + nota = "<- deve riprodurre ~1,64 (audit 02/07)" + elif cut == "2026-07-25": + nota = "<- il giorno della banda 1.312} {n_ho:>13} {full:>7.3f} {is_:>7.3f} {hold:>9.4f} {dd:>7.1%} {nota}") + + print("\n" + "=" * 96) + print(" VERDETTO (runtime)") + print("=" * 96) + rip_audit = abs(esiti["2026-07-02"] - 1.64) < 0.05 + rip_oggi = abs(esiti["oggi"] - 1.9223) < 0.01 + serie = [esiti[k] for k in esiti] + mono = all(b >= a - 0.03 for a, b in zip(serie, serie[1:])) + print(f" taglio 02/07 riproduce l'audit (~1,64)? {'SI' if rip_audit else 'NO'} " + f"({esiti['2026-07-02']:.4f})") + print(f" nessun taglio riproduce -> dato cambiato: " + f"{'no, il dato regge' if rip_audit else 'SI - INDAGARE IL FEED'}") + print(f" oggi riproduce il numero del test rosso? {'SI' if rip_oggi else 'NO'} " + f"({esiti['oggi']:.4f})") + print(f" la curva sale con la finestra (quasi-monot.)? {'SI' if mono else 'NO'}") + if rip_audit and rip_oggi: + print("\n => La deriva e' CRESCITA DELLA FINESTRA, non corruzione ne' codice: il test") + print(" confronta un numero che si muove col feed contro una banda scritta su una") + print(" finestra congelata. La riparazione giusta e' ancorare il test alla finestra") + print(" su cui la banda fu scritta (taglio fisso), non allargare la banda β€” una") + print(" banda che insegue il feed non riproduce niente (N11).") + + +if __name__ == "__main__": + main() diff --git a/src/live/monitor_health.py b/src/live/monitor_health.py index 947c42b..4ca0939 100644 --- a/src/live/monitor_health.py +++ b/src/live/monitor_health.py @@ -41,13 +41,14 @@ from __future__ import annotations import csv import json from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from pathlib import Path import numpy as np MIN_COVERAGE = 0.80 # sotto: la serie ha buchi -> non ci si decide sopra un gate MIN_BARS_PER_JUDGE = 2 # con una barra sola non esiste ne' coda ne' copertura +GRAZIA_PREMATURO_MIN = 5.0 # tolleranza fra chiusura della barra e mtime del file (clock/IO) @dataclass(frozen=True) @@ -60,6 +61,11 @@ class MonitorSpec: calendar: str = "crypto" # crypto (365g) | equity (giorni di borsa) max_age_h: float = 48.0 # oltre: FERMO gate: str | None = None + # True = i timestamp sono APERTURE di barra (convenzione resample_tf/ccxt/HL) -> vale il + # controllo PREMATURO. False = timbri di esecuzione (es. collect_chain scrive l'ora del + # giro): il timbro coincide con la scrittura per costruzione e il controllo mentirebbe + # SEMPRE (P14: una guardia piu' stretta del contratto insegna a ignorare la sezione). + open_labeled: bool = True MONITORS: tuple[MonitorSpec, ...] = ( @@ -78,7 +84,8 @@ MONITORS: tuple[MonitorSpec, ...] = ( # fissa ma una serie IRRECUPERABILE β€” un'ora non raccolta non si recupera da nessuna fonte, # quindi la soglia d'eta' e' stretta (3h = due giri persi). MonitorSpec("collect_chain", "chain_collect", "runs.jsonl", 1.0, max_age_h=3.0, - gate="serie irrecuperabile β€” la catena opzioni non si ricostruisce a posteriori"), + gate="serie irrecuperabile β€” la catena opzioni non si ricostruisce a posteriori", + open_labeled=False), ) @@ -130,14 +137,30 @@ def age_hours(last: datetime, now: datetime, calendar: str) -> float: return raw -def assess(spec: MonitorSpec, stamps: list[datetime], now: datetime) -> dict: +def assess(spec: MonitorSpec, stamps: list[datetime], now: datetime, + mtime: datetime | None = None) -> dict: """Verdetto su un monitor. Gli stati NON sono due: 'assente' e 'troppo giovane per un - giudizio' non sono 'OK', e non devono poter passare per tali.""" + giudizio' non sono 'OK', e non devono poter passare per tali. + + `mtime` = quando il file di serie e' stato scritto l'ultima volta. Serve al controllo + PREMATURO (r0822_monitor_audit): se l'ultima barra registrata CHIUDE dopo che il file e' + stato scritto, il monitor ha consumato una barra parziale β€” e' il difetto che ha fatto + registrare 4/41/2 minuti di mercato al giorno a 3 monitor con gate. Freschezza e copertura + NON possono vederlo: una serie fresca, completa e SBAGLIATA le passa entrambe.""" base = dict(name=spec.name, gate=spec.gate, n_bars=len(stamps), age_h=None, coverage=None, expected=None) if not stamps: return {**base, "status": "ASSENTE", "why": "nessuna serie leggibile: il monitor non ha mai scritto, o lo stato e' perso"} + if mtime is not None and spec.open_labeled: + chiude = stamps[-1] + timedelta(hours=spec.cadence_h) + ritardo_min = (chiude - mtime).total_seconds() / 60.0 + if ritardo_min > GRAZIA_PREMATURO_MIN: + return {**base, "age_h": age_hours(stamps[-1], now, spec.calendar), + "status": "PREMATURO", + "why": (f"l'ultima barra chiude alle {chiude:%Y-%m-%d %H:%M}Z ma il file e' " + f"stato scritto {ritardo_min/60:.1f}h PRIMA: registrata una barra " + "parziale β€” la serie va rigenerata, non solo riparata")} age = age_hours(stamps[-1], now, spec.calendar) if len(stamps) < MIN_BARS_PER_JUDGE: return {**base, "age_h": age, "status": "NUOVO", @@ -159,8 +182,11 @@ def check_all(root: Path, now: datetime | None = None, now = now or datetime.now(timezone.utc) out = [] for spec in monitors: - stamps = read_stamps(root / "data" / spec.directory / spec.series) - out.append(assess(spec, stamps, now)) + f = root / "data" / spec.directory / spec.series + stamps = read_stamps(f) + mtime = (datetime.fromtimestamp(f.stat().st_mtime, tz=timezone.utc) + if f.exists() else None) + out.append(assess(spec, stamps, now, mtime=mtime)) return out diff --git a/src/live/paper_guard.py b/src/live/paper_guard.py new file mode 100644 index 0000000..69410c3 --- /dev/null +++ b/src/live/paper_guard.py @@ -0,0 +1,64 @@ +"""paper_guard.py β€” solo barre CHIUSE nei forward-monitor. + +IL DIFETTO CHE QUESTO MODULO CHIUDE (misurato in r0822_monitor_audit, Β§5.1 di CLAUDE.md): +`fetch_hyperliquid`/`resample_tf` scrivono anche la barra del giorno IN CORSO; `advance()` dei +monitor consumava le barre con `ts > last_ts` β€” ultima inclusa β€” e portava `last_ts` sopra la +barra parziale. Risultato: `paper_statarb` registrava ~4 minuti di mercato al giorno, +`paper_dvolspread` ~2, `paper_xsr` ~41, e le 23 ore e mezza restanti di OGNI giorno non +entravano in nessun rendimento registrato. Il gate STATARB del 27/09 si ribaltava su 2 criteri +su 3 (Sharpe +1,96 registrato contro βˆ’2,02 ricostruito). + +LA REGOLA: una barra open-labeled (convenzione di TUTTO il progetto: `resample_tf` usa +`label='left'`, Hyperliquid e ccxt/Deribit timbrano all'apertura) e' CHIUSA quando +`ts + cadenza <= adesso`. L'orologio si LEGGE al momento della chiamata, mai cablato. + +D2: "una barra presente non e' una giornata presente" β€” questo modulo e' quella regola +trasformata in codice condiviso, cosi' il prossimo monitor non puo' ricommettere il difetto +dimenticandosene: importa `nuove_chiuse` e basta. +""" +from __future__ import annotations + +import numpy as np +import pandas as pd + +MS_1H = 3_600_000 +MS_1D = 86_400_000 + + +def _now_ms(now_ms: int | None) -> int: + if now_ms is not None: + return int(now_ms) + # stessa forma indipendente-dalla-risoluzione di `indice_chiuso` (D6) + epoch = pd.Timestamp("1970-01-01", tz="UTC") + return int((pd.Timestamp.now(tz="UTC") - epoch) // pd.Timedelta(milliseconds=1)) + + +def chiusa(ts_ms: int, cadenza_ms: int, now_ms: int | None = None) -> bool: + """True se la barra open-labeled `ts_ms` e' interamente nel passato.""" + return int(ts_ms) + int(cadenza_ms) <= _now_ms(now_ms) + + +def nuove_chiuse(ts, last_ts: int, cadenza_ms: int, now_ms: int | None = None) -> list[int]: + """Indici delle barre DOPO `last_ts` e gia' CHIUSE β€” il rimpiazzo della riga + `new = [i for i in range(len(ts)) if ts[i] > last_ts]` che ha rotto 4 monitor su 6.""" + now = _now_ms(now_ms) + ts = np.asarray(ts, dtype="int64") + return [int(i) for i in np.where((ts > int(last_ts)) & (ts + int(cadenza_ms) <= now))[0]] + + +def indice_chiuso(index: pd.DatetimeIndex, cadenza_ms: int, + now_ms: int | None = None) -> np.ndarray: + """Maschera 'barra chiusa' per un DatetimeIndex (i monitor a serie pandas: portfolio/combo). + + ⚠️ NIENTE `.asi8`/`.view("int64")`: la risoluzione dell'indice non e' garantita (pandas 3 + usa `us` di default, non `ns`) e un intero letto nella scala sbagliata passa SENZA eccezioni + β€” e' la lezione D6, gia' pagata, e ci si e' ricascati alla prima stesura di QUESTA funzione + (il test di fumo dava "chiusa" la barra del giorno in corso). La sottrazione dall'epoca con + `// Timedelta` e' indipendente dalla risoluzione: e' la stessa forma di `resample_tf`.""" + now = _now_ms(now_ms) + ix = pd.DatetimeIndex(index) + if ix.tz is None: + ix = ix.tz_localize("UTC") + epoch = pd.Timestamp("1970-01-01", tz="UTC") + ts = ((ix - epoch) // pd.Timedelta(milliseconds=1)).to_numpy() + return (ts + int(cadenza_ms)) <= now diff --git a/tests/conftest.py b/tests/conftest.py index ce15303..4141ed9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,4 +49,44 @@ def _isola_file_operativi_vivi(tmp_path, monkeypatch): """ import src.live.book as book monkeypatch.setattr(book, "EQUITY_WATERMARK", tmp_path / "equity_seen.json") + + # --- trades.db (2026-08-26, estensione Β§5.12) -------------------------------------------- + # ⚠️ `connect(path=DB_PATH)` cattura il default ALLA DEFINIZIONE: patchare la costante + # `DB_PATH` non cambierebbe niente. Si avvolge la funzione: un test che passa il path + # esplicito (tutti quelli esistenti usano `T.connect(tmp_path / ...)`) non cambia + # comportamento; un test che chiamasse `connect()` nudo finisce in tmp, non sul libro + # di bordo vivo. + import src.live.tradesdb as tradesdb + _connect_vero = tradesdb.connect + + def _connect_isolato(path=None): + return _connect_vero(tmp_path / "trades.db" if path is None else path) + + monkeypatch.setattr(tradesdb, "connect", _connect_isolato) + + # --- docs/journal/ ----------------------------------------------------------------------- + # `journal.scrivi_file` scrive pagine markdown TRACCIATE DA GIT: un test che lo chiama + # non deve poter sporcare il working tree (P16: il cron gira dal working tree). + import src.live.journal as journal + monkeypatch.setattr(journal, "JOURNAL_DIR", tmp_path / "journal") yield + + +@pytest.fixture(autouse=True, scope="session") +def _guardia_log_esecuzioni_vivo(): + """`book_executions.jsonl` e' scritto da `book_execute.py`, che i test caricano ad-hoc con + importlib: nessun monkeypatch di conftest puo' raggiungerlo PRIMA. Qui si fa l'altra meta' + del lavoro: impronta a inizio suite, verifica a fine suite. Se questo assert scatta, un + test ha scritto nel registro VIVO delle esecuzioni β€” trovarlo e dargli un path suo. + + Falso positivo possibile ma raro: un FILL REALE del cron :47 mentre la suite gira + (30 fill in 43 giorni). In quel caso l'orario dell'ultima riga del file coincide col + giro del cron, non con la suite: verificarlo prima di dare la colpa a un test.""" + f = PROJECT_ROOT / "data" / "live" / "book_executions.jsonl" + prima = (f.stat().st_mtime_ns, f.stat().st_size) if f.exists() else None + yield + dopo = (f.stat().st_mtime_ns, f.stat().st_size) if f.exists() else None + assert prima == dopo, ( + "un test ha scritto in data/live/book_executions.jsonl (registro VIVO): " + f"{prima} -> {dopo}. Se l'ultima riga del file e' delle :47 puo' essere un fill " + "reale del cron; altrimenti trovare il test e isolarlo.") diff --git a/tests/test_fee_sensitivity.py b/tests/test_fee_sensitivity.py index 576d186..87c39ad 100644 --- a/tests/test_fee_sensitivity.py +++ b/tests/test_fee_sensitivity.py @@ -79,17 +79,22 @@ def test_skh01_e_piu_sensibile_alla_fee_di_tp01(): # =========================================================================== -# guardia di decisione: la leva su cui poggia "liquidation fee irrilevante" +# guardia di decisione RIMOSSA (2026-08-26) β€” e il perche' va letto, non saltato # =========================================================================== -def test_leva_massima_da_config_resta_sotto_o_uguale_a_1x(): - """La conclusione 'liquidation fee 1% irrilevante' vale perche' il cap di live.json - limita il nozionale lordo a <= 1x l'equity (2 asset x equity/2), con disaster-SL a -30%. - Se il cap sale, quella conclusione NON vale piu': questo test deve rompersi.""" - cfg = json.loads((ROOT / "config" / "live.json").read_text()) - frac = cfg.get("max_notional_per_asset_frac") - assert frac is not None, "cap dinamico rimosso: rivedere l'analisi liquidation fee" - leva_max = frac * len(FS.ASSETS) - assert leva_max <= 1.0, ( - f"leva massima {leva_max:.2f}x > 1x: la liquidation fee 1% torna rilevante, " - "rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md") - assert cfg.get("disaster_sl_pct", 0) > 0, "disaster-SL disattivato: idem" +# Qui viveva `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`: verificava che +# `max_notional_per_asset_frac x n_asset <= 1.0`, a presidio della conclusione +# "liquidation fee 1% irrilevante" (r0726_fee_sensitivity). +# +# E' stato CANCELLATO, non aggiustato, per la ragione registrata in CLAUDE.md Β§5.5: +# misurava `frac x n_asset`, ma se il progetto introducesse la chiave di scala di +# SPEC-scale-key la grandezza vera diventerebbe `frac x n_asset x scala` β€” e questo +# test avrebbe CONTINUATO A PASSARE smettendo di controllare (P1: un sorvegliante che +# non deriva il bersaglio dal codice sorvegliato passa sempre). Una guardia che passa +# a bersaglio cambiato e' peggio di nessuna guardia: certifica una proprieta' che non +# vale piu'. +# +# La guardia GIUSTA e' gia' scritta nella spec: GATE SCALA-01 impone il tetto sul +# PRODOTTO `n_asset Β· frac Β· scala Β· disaster_sl_pct <= 0,50` β€” va cablata QUANDO (se) +# la chiave di scala entrera' in config, dentro il codice che la legge, non qui. +# Fino ad allora: la leva resta 1,0x per costruzione (nessuna chiave di scala esiste, +# verificato in Β§3 di CLAUDE.md) e il disaster-SL e' presidiato dai test del book. diff --git a/tests/test_monitor_health.py b/tests/test_monitor_health.py index 6f55046..22b9355 100644 --- a/tests/test_monitor_health.py +++ b/tests/test_monitor_health.py @@ -174,5 +174,66 @@ def test_i_monitor_reali_sono_leggibili_e_giudicati(): # legato al REGISTRO, non a una costante: aggiungere un monitor e' normale (30/07: # collect_chain), dimenticarne uno per strada no. assert len(rows) == len(MONITORS) >= 6 - assert all(r["status"] in ("OK", "NUOVO", "FERMO", "BUCATO", "ASSENTE") for r in rows) + assert all(r["status"] in ("OK", "NUOVO", "FERMO", "BUCATO", "ASSENTE", "PREMATURO") + for r in rows) assert {"paper_xsr", "paper_statarb", "paper_dvolspread"} <= {r["name"] for r in rows} + + +# =========================================================================== +# PREMATURO β€” barra registrata prima della sua chiusura (difetto Β§5.1) +# =========================================================================== +# La freschezza e la copertura NON possono vederlo: una serie fresca, completa e SBAGLIATA le +# passa entrambe (misurato: paper_statarb registrava 4 min/giorno con monitor_health "OK"). +# Controllo positivo in ENTRAMBI i versi, come raccomandato dall'audit. + +def _spec_1d(**kw): + from src.live.monitor_health import MonitorSpec + return MonitorSpec("t", "t", "returns.jsonl", 24.0, **kw) + + +def test_prematuro_si_accende_su_barra_scritta_prima_della_chiusura(): + from src.live.monitor_health import assess + stamps = [NOW.replace(hour=0, minute=0) - timedelta(days=2), + NOW.replace(hour=0, minute=0) - timedelta(days=1), + NOW.replace(hour=0, minute=0)] # barra del giorno IN CORSO + mtime = NOW.replace(hour=0, minute=35) # scritta alle 00:35 di oggi + r = assess(_spec_1d(), stamps, NOW, mtime=mtime) + assert r["status"] == "PREMATURO" + assert "rigenerata" in r["why"] + + +def test_prematuro_tace_su_una_serie_sana(): + from src.live.monitor_health import assess + stamps = [NOW.replace(hour=0, minute=0) - timedelta(days=3), + NOW.replace(hour=0, minute=0) - timedelta(days=2), + NOW.replace(hour=0, minute=0) - timedelta(days=1)] # ultima barra = IERI, chiusa + mtime = NOW.replace(hour=0, minute=35) # scritta stanotte alle 00:35 + r = assess(_spec_1d(), stamps, NOW, mtime=mtime) + assert r["status"] == "OK" + + +def test_prematuro_rispetta_la_grazia_di_clock(): + from src.live.monitor_health import assess + # barra chiusa alle 00:00, file scritto alle 23:58 di IERI (orologio del feed avanti di 2'): + # dentro la grazia di 5 min -> non e' un allarme + stamps = [NOW.replace(hour=0, minute=0) - timedelta(days=2), + NOW.replace(hour=0, minute=0) - timedelta(days=1)] + mtime = NOW.replace(hour=0, minute=0) - timedelta(minutes=2) + r = assess(_spec_1d(), stamps, NOW, mtime=mtime) + assert r["status"] != "PREMATURO" + + +def test_prematuro_non_giudica_i_timbri_di_esecuzione(): + from src.live.monitor_health import assess + # collect_chain timbra l'ORA DEL GIRO, non un'apertura di barra: il timbro coincide con + # la scrittura per costruzione e il controllo mentirebbe sempre (P14) -> open_labeled=False + stamps = [NOW - timedelta(hours=2), NOW - timedelta(hours=1)] + r = assess(_spec_1d(open_labeled=False), stamps, NOW, mtime=NOW - timedelta(hours=1)) + assert r["status"] != "PREMATURO" + + +def test_prematuro_senza_mtime_non_inventa_un_verdetto(): + from src.live.monitor_health import assess + stamps = [NOW.replace(hour=0, minute=0) - timedelta(days=1), NOW.replace(hour=0, minute=0)] + r = assess(_spec_1d(), stamps, NOW, mtime=None) + assert r["status"] != "PREMATURO" # senza mtime il controllo tace, non indovina diff --git a/tests/test_paper_guard.py b/tests/test_paper_guard.py new file mode 100644 index 0000000..9093187 --- /dev/null +++ b/tests/test_paper_guard.py @@ -0,0 +1,97 @@ +"""paper_guard: solo barre CHIUSE nei forward-monitor (riparazione del difetto Β§5.1). + +Il difetto: `advance()` consumava la barra del giorno in corso e 4 monitor su 6 registravano +minuti di mercato spacciati per giornate. La riparazione e' un filtro condiviso; qui si prova +il filtro nei due versi, la sua indipendenza dalla risoluzione dei timestamp (D6 β€” pagata +DI NUOVO alla prima stesura di questo stesso modulo), e che ogni monitor lo usi davvero. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from src.live.paper_guard import ( # noqa: E402 + MS_1D, MS_1H, chiusa, indice_chiuso, nuove_chiuse) + +NOW = int(pd.Timestamp("2026-08-26T00:35:00Z").value // 10**6) +OGGI = int(pd.Timestamp("2026-08-26T00:00:00Z").value // 10**6) +IERI = OGGI - MS_1D + + +def test_la_barra_del_giorno_in_corso_non_e_chiusa(): + assert chiusa(IERI, MS_1D, NOW) + assert not chiusa(OGGI, MS_1D, NOW) + + +def test_nuove_chiuse_salta_la_parziale_e_prende_la_chiusa(): + ts = [IERI - MS_1D, IERI, OGGI] + assert nuove_chiuse(ts, IERI - MS_1D, MS_1D, NOW) == [1] + # il giro successivo (dopo la mezzanotte seguente) la prende, una volta sola + assert nuove_chiuse(ts, IERI, MS_1D, NOW + MS_1D) == [2] + + +def test_nuove_chiuse_su_griglia_oraria(): + h0 = int(pd.Timestamp("2026-08-26T00:00:00Z").value // 10**6) + ts = [h0 - 2 * MS_1H, h0 - MS_1H, h0] # l'ultima e' l'ora IN CORSO alle 00:35 + assert nuove_chiuse(ts, h0 - 3 * MS_1H, MS_1H, NOW) == [0, 1] + + +def test_al_confine_esatto_la_barra_e_chiusa(): + # ts + cadenza == adesso: la barra e' finita in questo istante -> chiusa (<=, non <) + assert chiusa(IERI, MS_1D, OGGI) + + +def test_indice_chiuso_e_indipendente_dalla_risoluzione(): + """D6: `asi8`/`view('int64')` cambiano scala con la risoluzione dell'indice (pandas 3 usa + 'us'), e un intero nella scala sbagliata passa senza eccezioni. La prima stesura di + `indice_chiuso` e' caduta ESATTAMENTE qui: dava chiusa la barra del giorno in corso.""" + for unit in ("ns", "us", "s"): + ix = pd.DatetimeIndex( + np.array(["2026-08-25T00:00:00", "2026-08-26T00:00:00"], dtype=f"datetime64[{unit}]") + ).tz_localize("UTC") + m = indice_chiuso(ix, MS_1D, NOW) + assert list(m) == [True, False], (unit, list(m)) + + +def test_indice_naive_e_trattato_come_utc(): + ix = pd.DatetimeIndex(["2026-08-25", "2026-08-26"]) + assert list(indice_chiuso(ix, MS_1D, NOW)) == [True, False] + + +# =========================================================================== +# guardia sul SORGENTE: ogni monitor deve usare il filtro, non ridichiararlo +# =========================================================================== +# Stessa forma della guardia su cron_daily.sh: il difetto e' rientrato nel progetto ogni volta +# che una regola viveva solo nella memoria di chi l'aveva scritta (P1). + +MONITOR_1D = ("paper_xsr", "paper_statarb", "paper_dvolspread") +MONITOR_IDX = ("paper_portfolio", "paper_combo") + + +def _src(nome: str) -> str: + return (ROOT / "scripts" / "live" / f"{nome}.py").read_text() + + +def test_i_monitor_1d_usano_nuove_chiuse(): + for nome in MONITOR_1D: + s = _src(nome) + assert "PG.nuove_chiuse(" in s, f"{nome}: advance() non filtra le barre chiuse" + assert re.search(r"new = \[i for i in range\(len\(ts\)\) if ts\[i\] > st", s) is None, \ + f"{nome}: la vecchia riga che consumava la barra parziale e' ancora nel sorgente" + + +def test_prevday_filtra_le_ore_chiuse(): + s = _src("paper_prevday") + assert "PG.chiusa(" in s and "MS_1H" in s + + +def test_i_monitor_a_indice_usano_indice_chiuso(): + for nome in MONITOR_IDX: + assert "PG.indice_chiuso(" in _src(nome), f"{nome}: advance() non filtra i giorni chiusi" diff --git a/tests/test_paper_statarb.py b/tests/test_paper_statarb.py index ee1fdcf..80198a6 100644 --- a/tests/test_paper_statarb.py +++ b/tests/test_paper_statarb.py @@ -41,13 +41,19 @@ def test_advance_processes_forward_then_idempotent(tmp_path, monkeypatch): ts, *_ = ps._signal(j) st = _fresh_state(j, back=60) st = ps.advance(st, j) - assert st["n_bars"] == 60 # ha processato le 60 barre forward - assert st["last_ts"] == int(ts[-1]) + # ⚠️ 2026-08-26: l'attesa "== 60" codificava il DIFETTO Β§5.1 β€” l'ultima barra del + # pannello e' il giorno IN CORSO e advance() ora la salta finche' non e' chiusa. + # L'attesa giusta si DERIVA dal filtro di produzione, non si ridichiara (P1). + from src.live.paper_guard import MS_1D, nuove_chiuse + attese = len(nuove_chiuse(ts, int(ts[-61]), MS_1D)) + assert attese in (59, 60) # 60 solo se il pannello finisce su barra chiusa + assert st["n_bars"] == attese + assert st["last_ts"] == int(ts[attese - 61]) # la punta CHIUSA, non la punta del pannello assert st["cap_modeled"] != ps.MODELED_CAPITAL # l'equity si Γ¨ mossa # idempotente: ri-avanzare alla punta non aggiunge barre nΓ© muove l'equity eq_before = st["cap_modeled"] st2 = ps.advance(dict(st), j) - assert st2["n_bars"] == 60 + assert st2["n_bars"] == attese assert st2["cap_modeled"] == eq_before diff --git a/tests/test_wave_0726.py b/tests/test_wave_0726.py index bcfe29d..d4468de 100644 --- a/tests/test_wave_0726.py +++ b/tests/test_wave_0726.py @@ -9,6 +9,7 @@ import sys from pathlib import Path import numpy as np +import pandas as pd import pytest ROOT = Path(__file__).resolve().parents[1] @@ -41,13 +42,42 @@ def test_t1_le_modalita_condividono_le_stesse_trade_boundaries(): def test_t1_canonical_riproduce_il_backtest_ufficiale(): """La modalita' 'canonical' deve coincidere col backtest dello sleeve: e' l'ancora - di tutto il confronto. Tolleranza larga (il feed cresce ogni giorno).""" + di tutto il confronto. + + ⚠️ RISCRITTO il 2026-08-26 (era il rosso noto Β§5.9, indagato PRIMA di toccarlo β€” + `r0826_skh_band_drift.py`). La versione precedente confrontava una banda scritta + sulla finestra del 02/07 con un numero calcolato sul feed VIVO, che il cron allunga + ogni notte: l'hold-out e' passato da 1,64 a 1,93 senza che nulla si fosse rotto β€” + e la SOLA settimana 15-22/08 (rally ETH con SKH01 long) vale +0,35 di Sharpe + hold-out. Una banda che insegue il feed non riproduce niente: qui il feed viene + TAGLIATO alla data dell'audit, cosi' il test torna a essere una riproduzione + (N11) β€” si rompe se cambiano il CODICE o il DATO sulla finestra congelata, e per + nessun altro motivo. La deriva del vivo non e' piu' affare suo: per quella ci sono + i monitor.""" + import r0702_anchor_skh01 as r02 import r0726_skh_onbook as ob - s, _ = ob.skh_series(0, "canonical") - full, _, hold, dd = ob.sh3(s) - assert 1.2 < full < 1.7, full # audit 02/07: ~1.46 - assert 1.3 < hold < 1.9, hold # audit 02/07: ~1.64 - assert 0.12 < dd < 0.24, dd # audit 02/07: ~18.1% + CUT = int(pd.Timestamp("2026-07-02", tz="UTC").value // 10**6) + get5m_vero = r02.get5m + + def _tagliato(asset: str): + df = get5m_vero(asset) + return df[df["timestamp"] < CUT].reset_index(drop=True) + + r02.get5m = _tagliato + r02._CACHE.clear() # run_asset memoizza in un dict, non in lru_cache + r02.skh_port.cache_clear() + try: + s, _ = ob.skh_series(0, "canonical") + full, ins, hold, dd = ob.sh3(s) + finally: + r02.get5m = get5m_vero + r02._CACHE.clear() # le cache tagliate non devono avvelenare altri test + r02.skh_port.cache_clear() + # i numeri misurati da r0826_skh_band_drift al taglio 02/07 (== audit 02/07) + assert abs(full - 1.463) < 0.02, full + assert abs(ins - 1.424) < 0.02, ins + assert abs(hold - 1.6376) < 0.02, hold + assert abs(dd - 0.181) < 0.01, dd def test_t1_il_limit_tp_non_puo_riempire_peggio_del_livello():