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:
@@ -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).
|
||||||
@@ -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»
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user