monitor: advance() consuma solo barre CHIUSE — serie rigenerate, guardia PREMATURO, 4 debiti chiusi

S5.1 RIPARATO E RIGENERATO. Filtro condiviso src/live/paper_guard.py (barra
open-labeled chiusa = ts + cadenza <= adesso) importato da tutti e 6 i monitor;
serie rigenerate dallo stesso start_ts con scripts/live/paper_regen.py (evidenza
in *.pre_regen_20260826.*): statarb +1,95 -> -1,61 (il ribaltamento del gate
27/09 previsto dall'audit), dvolspread -14,73 -> -4,41, xsr -4,98 -> -2,72,
prevday invariato. Nessuna data di gate si sposta. Guardia cablata in
monitor_health: stato PREMATURO (ultima barra che chiude dopo l'mtime, grazia
5 min, open_labeled=False per collect_chain) — sul dato vivo segnala i 5 rotti
e tace sui 2 sani; dopo la rigenerazione 7/7 OK. paper_portfolio non rigenerato
(GTAA su ADJUSTED_LAST: replay != serie registrata, P12), tolta la coda non
chiusa. D6 pagata di nuovo nel fix: asi8 in pandas 3 e' in us, non ns —
blindata con test su tre risoluzioni.

S5.12 ESTESO: conftest devia anche trades.db (wrapper su connect: il default
e' catturato alla definizione) e docs/journal/; book_executions.jsonl
sorvegliato con impronta inizio/fine suite.

S5.5 FATTO: test_leva_massima cancellato con nota (misurava frac*n_asset:
con una chiave di scala avrebbe continuato a passare smettendo di controllare).

S5.9 INDAGATO E RIPARATO (r0826_skh_band_drift): il dato regge (taglio 02/07
riproduce l'audit 1,6376, in-sample identico su ogni taglio); la deriva era la
finestra hold-out — e la sola settimana 15-22/08 vale +0,35 di Sharpe hold-out.
Il test ora taglia il feed al 02/07 e verifica la riproduzione stretta.

Suite: 751 passati, 0 falliti.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
Adriano Dal Pastro
2026-08-26 10:12:23 +00:00
parent a51844875b
commit d597fc64e8
20 changed files with 872 additions and 54 deletions
+28 -21
View File
@@ -161,9 +161,9 @@ sull'hold-out; cambiarne le soglie guardando l'esito, pure.
| gate | data | criterio | stato | | 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) | | **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 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) | | **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** | | **`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 ## 5. Debiti aperti — difetti noti NON riparati
1. 🚨 **`advance()` registra una frazione del giorno → 4 monitor su 6 sono rotti.** 1. **RIPARATO E RIGENERATO (2026-08-26).** `advance()` consumava la barra del giorno in corso:
`fetch_hyperliquid`/`resample_tf` scrivono la barra del giorno **in corso**, `advance()` la consuma ora tutti e 6 i monitor filtrano con `src/live/paper_guard.nuove_chiuse` (barra chiusa =
e ci porta sopra `last_ts``paper_statarb` **4 min/giorno**, `paper_dvolspread` 2, `paper_xsr` 41. `ts + cadenza ≤ adesso`), le 4 serie rotte sono **rigenerate dallo stesso `start_ts`**
**La finestra non va persa** (misurato due volte: 0 barre chiuse cambiate su 1.761.259) → l'azione (`scripts/live/paper_regen.py`, vecchi file in `*.pre_regen_20260826.*`): statarb
e' *"riparare **e rigenerare**"*, e **nessuna data di gate si sposta**. Guardia raccomandata e non **+1,95 → 1,61**, dvolspread 14,73 → 4,41, xsr 4,98 → 2,72, prevday +0,39 → +0,40.
cablata: `ts_ultima_barra + cadenza ≤ mtime`, grazia 5 min. **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). 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 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 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 `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 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.* 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 5. **FATTO (2026-08-26):** `test_leva_massima_da_config...` cancellato con nota in
aggiustato:** misura `frac · n_asset` mentre con una chiave di scala la grandezza vera diventerebbe `tests/test_fee_sensitivity.py` misurava `frac · n_asset`, con una chiave di scala avrebbe
`frac · n_asset · scala`**continua a passare e smette di controllare**. 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 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 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**. 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 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 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*. 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** 9. **INDAGATO E RIPARATO (2026-08-26, `r0826_skh_band_drift.py`).** Il dato regge (taglio
(dal ~24-25/08): Sharpe hold-out SKH01 `canonical` **1,9223** contro la banda cablata al 02/07 riproduce l'audit: 1,6376; in-sample 1,424 identico su ogni taglio); la deriva era la
`1.3 < h < 1.9`. **Il codice non e' cambiato, sono cambiati i DATI**`data/raw/` e' gitignored finestra hold-out che si allunga — e 📌 **la SOLA settimana 15-22/08 vale +0,35 di Sharpe
e il cron lo ricostruisce ogni notte, quindi la finestra si allunga e il numero deriva (verso hold-out** (rally ETH con SKH01 long): il numero e' fragile, coerente con §2. Il test ora
l'**alto**). **La banda non e' stata allargata**: si guarda sotto prima di toccarla (lezione **taglia il feed al 02/07** e verifica la riproduzione stretta (±0,02): si rompe solo se
07/08). Suite: **730 passati, 1 fallito** (25/08). 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 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. 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' 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` 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 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 `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: ha gia' perso il 26/07 e il 21/08) + guardia in `test_cap_watermark.py`. ✅ **Esteso il
un test non deve poter scrivere in `data/live/`; oggi e' deviato solo il watermark, non 2026-08-26:** deviati anche `trades.db` (wrapper su `connect` — il default e' catturato alla
`trades.db` ne' `book_executions.jsonl`.** 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/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/journal.py # voce del giorno (numeri + lettura)
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare 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 ```python
@@ -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).
+20
View File
@@ -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 perche' `journal.py` **senza argomenti scrive OGGI**. La guardia **non vieta** di rigenerare
spesso: vieta di scrivere la pagina **una volta e lasciarla li'**. 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.** 🚨 **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 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» P&L** quando $1.399,39 erano il deposito USDC dell'operatore, e il «cumulato dall'arming»
+5
View File
@@ -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. 📌 **`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 Nessuna riga di codice chiede la barra chiusa — **se la sua griglia cambiasse diventerebbe rotto in
silenzio.** 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:** ✅ **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 `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 rieseguire, usa solo cio' che il monitor gia' scrive; segnala **5/6** e **tace sul sano**; controllo
+5
View File
@@ -18,6 +18,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd import numpy as np, pandas as pd
from src.portfolio.sleeves import _tp01_returns, _tp01_positions from src.portfolio.sleeves import _tp01_returns, _tp01_positions
from src.portfolio.gtaa import gtaa_returns, gtaa_weights 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_DIR = PROJECT_ROOT / "data" / "paper_combo"
STATE = STATE_DIR / "state.json" STATE = STATE_DIR / "state.json"
@@ -77,6 +78,10 @@ def advance():
return st return st
last = pd.Timestamp(st["last"]) last = pd.Timestamp(st["last"])
nn = naked[naked.index > last]; gg = guard[guard.index > 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): if len(nn):
e = st["equity"]; pk = st["peak"]; dd = st["max_dd"] e = st["equity"]; pk = st["peak"]; dd = st["max_dd"]
eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = [] eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = []
+4 -1
View File
@@ -85,6 +85,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "ortho")) sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "ortho"))
import altlib as al # noqa: E402 import altlib as al # noqa: E402
from src.live import paper_guard as PG # noqa: E402
import ortholib as ol # noqa: E402 import ortholib as ol # noqa: E402
from r0726_dvolspread_gate import make_book # noqa: E402 (fabbrica ESATTA del T2) 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: def advance(st: dict, P: dict) -> dict:
ts, dt = P["ts"], P["dt"] 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: if not new:
return st return st
hm, hr = st["wb_modeled"], st["wb_real"] # peso BTC TENUTO (w_eth = -w_btc) hm, hr = st["wb_modeled"], st["wb_real"] # peso BTC TENUTO (w_eth = -w_btc)
+5
View File
@@ -17,6 +17,7 @@ sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd import numpy as np, pandas as pd
from src.portfolio.portfolio import StrategyPortfolio from src.portfolio.portfolio import StrategyPortfolio
from src.portfolio.sleeves import active_sleeves from src.portfolio.sleeves import active_sleeves
from src.live import paper_guard as PG
STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio" STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio"
STATE = STATE_DIR / "state.json" STATE = STATE_DIR / "state.json"
@@ -51,6 +52,10 @@ def advance():
return st return st
last = pd.Timestamp(st["last"]) last = pd.Timestamp(st["last"])
new = r[r.index > 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): if len(new):
eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"] eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]
lines = [] lines = []
+5 -1
View File
@@ -38,6 +38,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT))
from src.backtest.harness import load # noqa: E402 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.prevday_breakout import target as prevday_target # noqa: E402
from src.strategies import prevday_breakout as pb # 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, dt=pd.to_datetime(df["datetime"]).values, r=r,
tgt=prevday_target(df)) tgt=prevday_target(df))
common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"])) 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: if not new_ts:
return st return st
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS} idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
+228
View File
@@ -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_<data>.* (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()
+4 -1
View File
@@ -51,6 +51,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
# Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest). # Segnale ESATTO dello sweep (nessuna reimplementazione → identità garantita col backtest).
from orthogonal_signals import build_joint, f_statarb_resid, spread_ret # noqa: E402 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_DIR = PROJECT_ROOT / "data" / "paper_statarb"
STATE_FILE = STATE_DIR / "state.json" 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: def advance(st: dict, j: pd.DataFrame) -> dict:
ts, dt, pos, sr = _signal(j) 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: if not new:
return st return st
pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente) pm, pr = st["pos_modeled"], st["pos_real"] # posizioni TENUTE (decise alla barra precedente)
+4 -1
View File
@@ -65,6 +65,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
# Segnale ESATTO dello studio (nessuna reimplementazione -> niente drift col backtest). # 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 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_DIR = PROJECT_ROOT / "data" / "paper_xsr"
STATE_FILE = STATE_DIR / "state.json" 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)}): " print(f" [XSR01] universo cambiato ({len(st['syms'])} -> {len(syms)}): "
"la finestra forward richiede universo costante. Usa --reset per ripartire.") "la finestra forward richiede universo costante. Usa --reset per ripartire.")
return st 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: if not new:
return st return st
for i in new: for i in new:
+111
View File
@@ -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.3<h<1.9"
elif cut is None:
nota = "<- deve riprodurre l'1,9223 del test rosso"
lab = cut or "oggi"
esiti[lab] = hold
print(f" {lab:>12} {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()
+32 -6
View File
@@ -41,13 +41,14 @@ from __future__ import annotations
import csv import csv
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
MIN_COVERAGE = 0.80 # sotto: la serie ha buchi -> non ci si decide sopra un gate 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 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) @dataclass(frozen=True)
@@ -60,6 +61,11 @@ class MonitorSpec:
calendar: str = "crypto" # crypto (365g) | equity (giorni di borsa) calendar: str = "crypto" # crypto (365g) | equity (giorni di borsa)
max_age_h: float = 48.0 # oltre: FERMO max_age_h: float = 48.0 # oltre: FERMO
gate: str | None = None 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, ...] = ( 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, # 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). # 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, 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 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 """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), base = dict(name=spec.name, gate=spec.gate, n_bars=len(stamps),
age_h=None, coverage=None, expected=None) age_h=None, coverage=None, expected=None)
if not stamps: if not stamps:
return {**base, "status": "ASSENTE", return {**base, "status": "ASSENTE",
"why": "nessuna serie leggibile: il monitor non ha mai scritto, o lo stato e' perso"} "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) age = age_hours(stamps[-1], now, spec.calendar)
if len(stamps) < MIN_BARS_PER_JUDGE: if len(stamps) < MIN_BARS_PER_JUDGE:
return {**base, "age_h": age, "status": "NUOVO", 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) now = now or datetime.now(timezone.utc)
out = [] out = []
for spec in monitors: for spec in monitors:
stamps = read_stamps(root / "data" / spec.directory / spec.series) f = root / "data" / spec.directory / spec.series
out.append(assess(spec, stamps, now)) 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 return out
+64
View File
@@ -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
+40
View File
@@ -49,4 +49,44 @@ def _isola_file_operativi_vivi(tmp_path, monkeypatch):
""" """
import src.live.book as book import src.live.book as book
monkeypatch.setattr(book, "EQUITY_WATERMARK", tmp_path / "equity_seen.json") 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 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.")
+18 -13
View File
@@ -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(): # Qui viveva `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`: verificava che
"""La conclusione 'liquidation fee 1% irrilevante' vale perche' il cap di live.json # `max_notional_per_asset_frac x n_asset <= 1.0`, a presidio della conclusione
limita il nozionale lordo a <= 1x l'equity (2 asset x equity/2), con disaster-SL a -30%. # "liquidation fee 1% irrilevante" (r0726_fee_sensitivity).
Se il cap sale, quella conclusione NON vale piu': questo test deve rompersi.""" #
cfg = json.loads((ROOT / "config" / "live.json").read_text()) # E' stato CANCELLATO, non aggiustato, per la ragione registrata in CLAUDE.md §5.5:
frac = cfg.get("max_notional_per_asset_frac") # misurava `frac x n_asset`, ma se il progetto introducesse la chiave di scala di
assert frac is not None, "cap dinamico rimosso: rivedere l'analisi liquidation fee" # SPEC-scale-key la grandezza vera diventerebbe `frac x n_asset x scala` — e questo
leva_max = frac * len(FS.ASSETS) # test avrebbe CONTINUATO A PASSARE smettendo di controllare (P1: un sorvegliante che
assert leva_max <= 1.0, ( # non deriva il bersaglio dal codice sorvegliato passa sempre). Una guardia che passa
f"leva massima {leva_max:.2f}x > 1x: la liquidation fee 1% torna rilevante, " # a bersaglio cambiato e' peggio di nessuna guardia: certifica una proprieta' che non
"rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md") # vale piu'.
assert cfg.get("disaster_sl_pct", 0) > 0, "disaster-SL disattivato: idem" #
# 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.
+62 -1
View File
@@ -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: # legato al REGISTRO, non a una costante: aggiungere un monitor e' normale (30/07:
# collect_chain), dimenticarne uno per strada no. # collect_chain), dimenticarne uno per strada no.
assert len(rows) == len(MONITORS) >= 6 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} 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
+97
View File
@@ -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"
+9 -3
View File
@@ -41,13 +41,19 @@ def test_advance_processes_forward_then_idempotent(tmp_path, monkeypatch):
ts, *_ = ps._signal(j) ts, *_ = ps._signal(j)
st = _fresh_state(j, back=60) st = _fresh_state(j, back=60)
st = ps.advance(st, j) st = ps.advance(st, j)
assert st["n_bars"] == 60 # ha processato le 60 barre forward # ⚠️ 2026-08-26: l'attesa "== 60" codificava il DIFETTO §5.1 — l'ultima barra del
assert st["last_ts"] == int(ts[-1]) # 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 assert st["cap_modeled"] != ps.MODELED_CAPITAL # l'equity si è mossa
# idempotente: ri-avanzare alla punta non aggiunge barre né muove l'equity # idempotente: ri-avanzare alla punta non aggiunge barre né muove l'equity
eq_before = st["cap_modeled"] eq_before = st["cap_modeled"]
st2 = ps.advance(dict(st), j) st2 = ps.advance(dict(st), j)
assert st2["n_bars"] == 60 assert st2["n_bars"] == attese
assert st2["cap_modeled"] == eq_before assert st2["cap_modeled"] == eq_before
+35 -5
View File
@@ -9,6 +9,7 @@ import sys
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
import pandas as pd
import pytest import pytest
ROOT = Path(__file__).resolve().parents[1] 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(): def test_t1_canonical_riproduce_il_backtest_ufficiale():
"""La modalita' 'canonical' deve coincidere col backtest dello sleeve: e' l'ancora """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 import r0726_skh_onbook as ob
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") s, _ = ob.skh_series(0, "canonical")
full, _, hold, dd = ob.sh3(s) full, ins, hold, dd = ob.sh3(s)
assert 1.2 < full < 1.7, full # audit 02/07: ~1.46 finally:
assert 1.3 < hold < 1.9, hold # audit 02/07: ~1.64 r02.get5m = get5m_vero
assert 0.12 < dd < 0.24, dd # audit 02/07: ~18.1% 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(): def test_t1_il_limit_tp_non_puo_riempire_peggio_del_livello():