revisione 02/09, seconda tornata: il classificatore dei movimenti era cieco ~23 ore al giorno

Il feed 1h certificato si ferma alle 00:00: per le letture successive `asof` dava la stessa
barra a t0 e t1, mercato "fermo" = 0, e qualunque calo >=10% del giorno sarebbe stato un
"movimento" scorporato come prelievo (giornale dal 25/08, report da oggi). Con feed assente
tutto era "ambiguo" e il report stampava e1/e0-1 sotto l'etichetta TWR.

- journal.movimenti_capitale: stato "mercato non misurabile" (feed fermo/assente/barra
  mancante) => ambiguo con la ragione; leva_tetto = min(frac x n x scala, LEVA_LORDA_MAX) (P1).
- journal.rendimento_twr: salto non classificabile => twr E trading None con motivo.
- journal.pnl_giorno CHIAMA rendimento_twr (prima rifaceva cum - certi: "entrambi la chiamano"
  era falso); la pagina stampa il TWR; analista qualifica il cumulato "di cui versati".
- trades_db --report: la classe di ogni salto con la sua misura (mercato max a leva piena).
- book_execute docstring: banda dello stop -33,5%/-26,5% (era invertita), "non costa ordini"
  -> micro-ordini di ri-taglia (22/48, C2), latenza <=1h solo con feed fresca, conteggi di
  r0823 al posto di "raddoppia" (2 contro 1 e' rotolante vs pavimento sulla riga 4h; la
  peggiore misurata e' 24h).
- test_book_cadenza: LTF_MIN importato da skyhook; tutte le righe attive della crontab (una
  sola); tre stati dello skip (col progetto ma senza cron_book => ROSSO); slot di release
  testato con venue_probe (il :07 come controllo positivo); parser regolare, passo > 0.
- r0823_sl_anchor: guardia sul :47 (era sul vecchio :07), prosa al passato; cron_chain.sh idem.
- CLAUDE.md: §5.7 frase invertita corretta, §5.14 limite vero (non l'artefatto della fixture),
  §2 ora della LETTURA (13:47Z), nuovo debito §5.15 (USDE fuori dal bound di mercato).
- diari: tempi "scritto" corretti coi commit; sezione "Seconda tornata".

Test +11 (858 verdi).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
This commit is contained in:
Adriano Dal Pastro
2026-09-02 15:33:13 +00:00
parent 835e0c8666
commit 1279ea605a
13 changed files with 328 additions and 90 deletions
+36 -13
View File
@@ -115,7 +115,7 @@ sono all'ancora fortunata; le stime oneste sono la **mediana della banda d'ancor
| P(≥50 €/g), canale funded, 36 mesi | 42% | **2,6%** [1,5 4,7], P(zero) 40,3% | | P(≥50 €/g), canale funded, 36 mesi | 42% | **2,6%** [1,5 4,7], P(zero) 40,3% |
| XSR01 | Sharpe 1,82 (e' una **terza** lente, divisore fisso 50) | **1,79** alla scoperta / **1,56-1,63** a oggi — lente dei gate, sole barre chiuse. Citare sempre la coppia (lente, ultima barra chiusa) | | XSR01 | Sharpe 1,82 (e' una **terza** lente, divisore fisso 50) | **1,79** alla scoperta / **1,56-1,63** a oggi — lente dei gate, sole barre chiuse. Citare sempre la coppia (lente, ultima barra chiusa) |
| soffitto direzionale BTC/ETH | ~1,3 | **~1,15** col funding dentro | | soffitto direzionale BTC/ETH | ~1,3 | **~1,15** col funding dentro |
| performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) | **TWR +10,61%** spezzato sul versamento — +11,61% fino al 25/08, 0,89% dopo (02/09 14:02Z). Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso) | | performance del libro LIVE | «+243%» di equity (e' **96,3% un bonifico**) | **TWR +10,61%** spezzato sul versamento — +11,61% fino al 25/08, 0,89% dopo (lettura 02/09 13:47Z). Dal 02/09 lo stampa `trades_db.py --report` (debito 14 chiuso) |
📌 **Il libro a k=1 rende MENO dell'S&P 500** (15,19% contro 17,40%, stessa finestra): il vantaggio 📌 **Il libro a k=1 rende MENO dell'S&P 500** (15,19% contro 17,40%, stessa finestra): il vantaggio
sta nello **Sharpe** (1,35 vs 0,89), e **senza leva non si converte in rendimento**. A iso-rischio sta nello **Sharpe** (1,35 vs 0,89), e **senza leva non si converte in rendimento**. A iso-rischio
@@ -283,17 +283,22 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
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**.
7.**RIPARATO (2026-09-02).** Il docstring di `book_execute.py` prescriveva "ogni ~230 minuti" 7.**RIPARATO (2026-09-02).** Il docstring di `book_execute.py` prescriveva "ogni ~230 minuti"
mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi lo mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi
avesse "corretto" nel verso del cron avrebbe spostato il libro sulla riga 4h, dove BTC avesse "corretto" il **CRON** verso il docstring avrebbe spostato il libro dalla riga 1h (rotolante:
**raddoppia** gli scatti del disaster-SL (`r0823_sl_anchor.py`). Ora il docstring dichiara BTC 0 scatti / ETH 1 in 7-8 anni) alla riga 4h (BTC 2, contro 1 del pavimento); la peggiore
misurata e' 24h (ETH 5, con 2 in 30 giorni) — `r0823_sl_anchor.py`. Ora il docstring dichiara
**CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h; il rotolante e' **CADENZA: ORARIA** con la ragione (giro idempotente: latenza SKH01 ≤1h; il rotolante e'
**controllato** ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl`, mark **controllato** ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl`, mark
+5,263%/4,762% o taglia >10% — «si ri-ancora ogni ora» era una frase sbagliata, corretta in +5,263%/4,762% o taglia >10%, quindi lo stop siede fra **33,5% e 26,5%** dal mark corrente —
revisione il 02/09) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene «si ri-ancora ogni ora» era una frase sbagliata, corretta in revisione il 02/09; e «girare piu'
fitto non costa ordini» pure: 22 dei 48 ordini live sono ri-taglie |Δ|≤$10, deadband in valuta
assoluta, C2) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene
d'accordo le tre dichiarazioni — docstring, riga in `cron_book.sh`, **crontab installata** d'accordo le tre dichiarazioni — docstring, riga in `cron_book.sh`, **crontab installata**
(letta con `crontab -l`; dove non e' leggibile il test si dichiara SALTATO, non verde) — e (letta con `crontab -l`, **tutte** le righe attive: due righe sono due esecuzioni; tre stati —
verifica che la cadenza dichiarata sia 60 min, sotto la griglia 230m e lontana dai 240 della illeggibile ⇒ SALTATO, leggibile senza questo progetto ⇒ SALTATO, leggibile col progetto ma senza
riga peggiore. Diario `2026-09-02b-debito-7-cadenza-docstring.md`. `cron_book.sh` attivo ⇒ **ROSSO**) — verifica 60 min < `skyhook.LTF_MIN` 230 < 240 (riga 4h),
minuto ≠ :00 **e fuori dallo slot di release** (`venue_probe.in_release_window`, col :07 come
controllo positivo). Diario `2026-09-02b-debito-7-cadenza-docstring.md`.
8. ⚠️ **Il Rulebook Deribit** (ADL, perdita socializzata, *emergency powers*, conti dormienti) **non 8. ⚠️ **Il Rulebook Deribit** (ADL, perdita socializzata, *emergency powers*, conti dormienti) **non
lo sorveglia nessuno**; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli. lo sorveglia nessuno**; `MAINT_GRACE_HOURS`=2 **presume** gli annunci invece di leggerli.
*Ridotto in parte il 2026-08-25:* `src/live/venue_probe.py` interroga l'API **pubblica** *Ridotto in parte il 2026-08-25:* `src/live/venue_probe.py` interroga l'API **pubblica**
@@ -348,10 +353,28 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
elencati, e il delta $ grezzo etichettato «movimenti INCLUSI» — il `%` grezzo **non compare piu'**. elencati, e il delta $ grezzo etichettato «movimenti INCLUSI» — il `%` grezzo **non compare piu'**.
Test: 5 in `test_journal.py` (fra cui la riproduzione del +10,80% del diario 01/09, M23) + 2 in Test: 5 in `test_journal.py` (fra cui la riproduzione del +10,80% del diario 01/09, M23) + 2 in
`test_trades_report.py` sul testo stampato, con `connect()` deviato in tmp. Diario `test_trades_report.py` sul testo stampato, con `connect()` deviato in tmp. Diario
`2026-09-02-debito-14-report-twr.md`. ⚠️ **Due limiti dichiarati dalla revisione del 02/09** (D5): (a) **l'intervallo che contiene un movimento certo esce INTERO dal rendimento** — il suo P&L di mercato finisce in `certi` e fuori da `trading`, con errore massimo pari a META' del movimento riconosciuto per costruzione del classificatore (il 25/08: ~$0,5 su $1.399); non si stima, si dichiara (P12); (b) **a base di equity zero `twr` E `trading` sono None**: il salto 0→X del primo versamento e' invisibile al classificatore e `trading` varrebbe l'intero conto. ⚠️ **Limite ereditato (D5):** un +10% di `2026-09-02-debito-14-report-twr.md`. ⚠️ **Due limiti dichiarati dalla revisione del 02/09** (D5): (a) **l'intervallo che contiene un movimento certo esce INTERO dal rendimento** — il suo P&L di mercato finisce in `certi` e fuori da `trading`, con errore massimo pari a META' del movimento riconosciuto per costruzione del classificatore (il 25/08: ~$0,5 su $1.399); non si stima, si dichiara (P12); (b) **a base di equity zero `twr` E `trading` sono None**: il salto 0→X del primo versamento e' invisibile al classificatore e `trading` varrebbe l'intero conto. 🚨 **Seconda tornata (02/09, 15:30Z): il classificatore era CIECO ~23 ore al
trading fra due letture consecutive tocca la soglia del rilevatore e, a mercato fermo, verrebbe giorno.** Il feed 1h certificato si ferma alle 00:00 (rebuild 00:30): per le letture successive
classificato «movimento» — e' lo stesso limite del rilevatore live, non un difetto nuovo. `asof` dava la stessa barra a t0 e t1, mercato "fermo" = 0, e **qualunque calo ≥10% del giorno
sarebbe stato un «movimento»** — un crash o un depeg USDE stampato come prelievo, nel giornale
(dal 25/08) e nel report. Riprodotto su copia del DB. Ora un salto fuori dalla copertura del feed
(o con feed assente) e' **«mercato non misurabile»** ⇒ `ambiguo` con la ragione, e `twr`/`trading`
sono **None** con motivo (prima, a feed assente, `e1/e0-1` tornava sotto l'etichetta TWR). Il
«limite ereditato +10% a mercato fermo» scritto qui la mattina era un **artefatto della fixture
piatta**, non un limite del classificatore (con mercato misurato il trading non puo' superare
2·max_mkt): cancellato. E `pnl_giorno` ora **chiama** `rendimento_twr` (prima l'affermazione
«la funzione che entrambi chiamano» era falsa: il giornale rifaceva `cum certi`); la pagina
stampa il TWR accanto al cumulato e la testata Telegram dell'analista qualifica il cumulato
«di cui versati». `leva_tetto` del classificatore include la scala e il tetto di codice (P1).
15. ⚠️ **Il bound di mercato del classificatore dei movimenti guarda solo BTC/ETH, ma dal 26/08 il
31% dell'equity e' USDE** (trovato in revisione il 02/09). Un depeg dell'USDE a BTC/ETH fermi:
1,5% (indice 0,95) resta sotto soglia e nel P&L; una rottura piena (31%) supera 2× un mercato
"fermo" e diventerebbe un **«movimento» scorporato come prelievo**, nel giornale e nel report —
l'invenzione silenziosa che P12 vieta. Il bound corretto somma `quota_usde × |Δ indice usde_usdc|`,
ma l'indice e' registrato **una volta al giorno** (`usde_watch`, 12:35Z): serve una serie oraria
(`balance_watch` la potrebbe portare) prima di chiuderlo. Intanto: chi legge un «movimento»
negativo senza bonifico in uscita **lo verifichi contro `usde_watch.jsonl`** prima di crederci.
--- ---
@@ -569,7 +592,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 (847, tutti verdi al 02/09) uv run pytest # test (858, tutti verdi al 02/09)
``` ```
```python ```python
+16 -3
View File
@@ -1,7 +1,6 @@
# 2026-09-02 — Debito 14: il report stampa il TWR, non il bonifico # 2026-09-02 — Debito 14: il report stampa il TWR, non il bonifico
*Scritto il 2026-09-02 fra le 14:16Z e le 14:30Z. Numeri dalla serie `equity` di `trades.db` *Scritto il 2026-09-02 fra le 14:16Z e le 14:22Z (commit `2f701b8`, 14:22:50Z; la prima stesura diceva «fino alle 14:30Z» senza averlo letto — corretto in revisione). Numeri dalla lettura di equity delle 13:47:01Z (1.678 letture) e dal report rilanciato dopo la riparazione.*
(1.678 letture) e dal report rilanciato dopo la riparazione.*
## Cosa c'era ## Cosa c'era
@@ -52,7 +51,7 @@ nel test, nel CLAUDE.md e qui.
- Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già - Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già
fuori scope il 01/09 e lo resta. fuori scope il 01/09 e lo resta.
## Revisione del codice (15:20Z) — tre cose trovate, tutte riparate ## Revisione del codice, prima tornata (15:15Z, commit `835e0c8` 15:17:05Z) — tre cose trovate, tutte riparate
| trovato | vero? | riparazione | | trovato | vero? | riparazione |
|---|---|---| |---|---|---|
@@ -61,3 +60,17 @@ nel test, nel CLAUDE.md e qui.
| movimento nel primo intervallo o due consecutivi → segmento `+0.00% [x -> x]` | sì, cosmetico | i segmenti a lunghezza zero non si producono; nessun tempo a mercato → TWR 0,0 dichiarato numero. Test | | movimento nel primo intervallo o due consecutivi → segmento `+0.00% [x -> x]` | sì, cosmetico | i segmenti a lunghezza zero non si producono; nessun tempo a mercato → TWR 0,0 dichiarato numero. Test |
Il numero pubblicato non cambia: +10,61% alle 14:02Z, +10,56% alle 14:47Z per la marcatura. Il numero pubblicato non cambia: +10,61% alle 14:02Z, +10,56% alle 14:47Z per la marcatura.
## Seconda tornata (15:30Z) — la piu' importante di tutte
| trovato | vero? | riparazione |
|---|---|---|
| il classificatore dei movimenti era **cieco ~23 ore al giorno**: il feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, qualunque calo ≥10% del giorno diventava «movimento». Riprodotto dalla revisione su copia del DB: 12% appeso alle 14:47 → `certi` 1.399 → 1.153, TWR invariato invece di 2,71% | si'. Verificato: ultima barra 00:00, adesso 15:26 | nuovo stato **«mercato non misurabile»** (feed fermo, assente, o barra mancante) ⇒ `ambiguo` con la ragione; `twr` e `trading` None con motivo |
| con `data/raw` assente (host ripristinato prima del rebuild) tutto era «ambiguo», `certi` 0, e il report stampava **+243% sotto l'etichetta TWR** | si' | stesso stato: n/d, mai il grezzo |
| «`rendimento_twr` e' la funzione che entrambi chiamano» era **falso**: `pnl_giorno` rifaceva `cum certi`, e la testata Telegram mandava il cumulato grezzo | si' | `pnl_giorno` chiama `rendimento_twr`; la pagina stampa il TWR accanto al cumulato; la testata dice «di cui versati → trading» |
| `leva_tetto` del classificatore ridichiarava `frac × n` ignorando `book_scale_k` e `LEVA_LORDA_MAX` (P1) | si' | `min(frac × n × scala, LEVA_LORDA_MAX)`; fallback = tetto di codice |
| il «limite ereditato: +10% di trading a mercato fermo» scritto la mattina era un **artefatto della fixture piatta** (a mercato misurato il trading non supera 2·max_mkt) | si' | cancellato ovunque; il limite vero e' quello sopra |
| il bound di mercato guarda solo BTC/ETH, ma il 31% dell'equity e' USDE: un depeg pieno sarebbe un «prelievo» | si' | **non riparato**: l'indice USDE e' registrato una volta al giorno. Debito **§5.15** |
| l'ora del report scritta in §2 (14:02Z) era l'ora di esecuzione, non quella della lettura (13:47:01Z); i tempi «scritto» dei diari erano posteriori ai commit | si' | corretti coi tempi veri |
Il numero pubblicato: TWR +10,61% alla lettura delle 13:47Z; +10,56% alle 14:47Z.
@@ -1,6 +1,6 @@
# 2026-09-02 — Debito 7: il docstring diceva 230 minuti, il cron gira ogni ora # 2026-09-02 — Debito 7: il docstring diceva 230 minuti, il cron gira ogni ora
*Scritto il 2026-09-02 alle 14:45Z. Cadenza misurata dal log (`r0823_sl_anchor.py`: 1.443 giri in *Scritto il 2026-09-02 fra le 14:36Z e le 14:38Z (commit `861cc7f`, 14:38:53Z; la prima stesura diceva «alle 14:45Z» senza averlo letto — corretto in revisione). Cadenza misurata dal log (`r0823_sl_anchor.py`: 1.443 giri in
1.442 ore) e dalla crontab installata, letta dal test.* 1.442 ore) e dalla crontab installata, letta dal test.*
## Il difetto ## Il difetto
@@ -44,9 +44,24 @@ minuti) BTC **raddoppia** gli scatti del disaster-SL rotolante (2 contro 1; a 24
- Non ho aggiunto un sorvegliante a runtime sulla cadenza (contare i giri per ora nel log): il - Non ho aggiunto un sorvegliante a runtime sulla cadenza (contare i giri per ora nel log): il
giornale già stampa «giri mancanti» ogni giorno, e il test copre la deriva di configurazione. giornale già stampa «giri mancanti» ogni giorno, e il test copre la deriva di configurazione.
## Revisione del codice (15:20Z) — due cose trovate, tutte riparate ## Revisione del codice, prima tornata (15:15Z, commit `835e0c8`) — due cose trovate, tutte riparate
| trovato | vero? | riparazione | | trovato | vero? | riparazione |
|---|---|---| |---|---|---|
| il docstring nuovo diceva «il disaster-SL rotolante si ri-ancora ogni ora»: falso. `ensure_disaster_sl` lascia il bracket com'è finché lo stop voluto è entro il 5% da quello piazzato e la taglia entro il 10%; il giro orario **controlla**, ri-ancora oltre la tolleranza (mark +5,263% / 4,762%). I «BTC 0 / ETH 1» di `r0823` sono misurati **con** questa isteresi | sì, verificato in `execution.py:233-235`. Propagato anche a CLAUDE.md §5.7 e memoria 40 | riscritto in tutti e tre i posti: «controllato ogni ora, ri-ancorato solo oltre la tolleranza; lo stop siede fra 26,3% e 33,3% dal mark corrente». Frase sbagliata su un meccanismo di sicurezza vivo: era la segnalazione più importante | | il docstring nuovo diceva «il disaster-SL rotolante si ri-ancora ogni ora»: falso. `ensure_disaster_sl` lascia il bracket com'è finché lo stop voluto è entro il 5% da quello piazzato e la taglia entro il 10%; il giro orario **controlla**, ri-ancora oltre la tolleranza (mark +5,263% / 4,762%). I «BTC 0 / ETH 1» di `r0823` sono misurati **con** questa isteresi | sì, verificato in `execution.py:233-235`. Propagato anche a CLAUDE.md §5.7 e memoria 40 | riscritto in tutti e tre i posti: «controllato ogni ora, ri-ancorato solo oltre la tolleranza; lo stop siede fra 26,3% e 33,3% dal mark corrente». Frase sbagliata su un meccanismo di sicurezza vivo: era la segnalazione più importante |
| la guardia del test cercava due letterali («ogni ~230»): «ogni quattro ore» sarebbe passato | sì | la guardia estrae **ogni** prescrizione «ogni/every N unità» e pretende 60 minuti; controllo positivo (M15) su cinque frasi. Limite dichiarato (P13): una prosa che prescrive senza «ogni» passa | | la guardia del test cercava due letterali («ogni ~230»): «ogni quattro ore» sarebbe passato | sì | la guardia estrae **ogni** prescrizione «ogni/every N unità» e pretende 60 minuti; controllo positivo (M15) su cinque frasi. Limite dichiarato (P13): una prosa che prescrive senza «ogni» passa |
## Seconda tornata (15:30Z)
| trovato | vero? | riparazione |
|---|---|---|
| la frase nuova di CLAUDE.md §5.7 era **invertita** («chi lo avesse corretto nel verso del cron»): correggere il docstring verso il cron e' proprio la riparazione; il pericolo e' correggere il CRON verso il docstring | si' | riscritta nel verso giusto |
| la banda dello stop era sbagliata: con isteresi 5% sullo stop (0,70·mark) lo stop siede a 0,6650,735 × mark, cioe' **33,5% / 26,5%**, non 26,3/33,3 | si' | corretta in docstring, CLAUDE.md, memoria |
| «BTC raddoppia gli scatti» leggeva male `r0823`: il «2 contro 1» e' rotolante contro pavimento **dentro la riga 4h**; a 1h il rotolante ne fa 0, e la riga peggiore misurata e' **24h** (ETH 5) | si' (la revisione ha eseguito `r0823.simula`) | riscritto ovunque coi conteggi; nel test `RIGA_4H_MIN`, non «peggiore»; `LTF_MIN` importato da `skyhook` (P1) |
| «girare piu' fitto non costa ordini» era falso: 22 dei 48 ordini live sono ri-taglie |Δ|≤$10 a segnale invariato; il deadband e' in valuta assoluta (C2) | si' (contati) | riscritto: «costa solo micro-ordini di ri-taglia», con la regola C2 |
| «latenza ≤1h» senza condizione: vale solo con la feed 5m fresca; `fresh_5m` ripiega in silenzio sul giornaliero e `skh_feed_max_age_min` allerta e basta | si' | condizione scritta |
| `_cron_installato` leggeva la **prima** riga: una seconda riga attiva sotto (`17 * * * *`, dentro lo slot di release) era invisibile | si' (simulato) | si contano **tutte** le righe attive, e devono essere una |
| lo skip collassava tre stati: crontab illeggibile, leggibile senza il progetto, leggibile col progetto ma senza `cron_book.sh` attivo (= libro non schedulato) | si' | tre stati: SALTATO, SALTATO, **ROSSO** |
| solo il vincolo 1 del `:47` era testato (minuto ≠ 00); il vincolo 2 (fuori dallo slot di release del martedi') no | si' | test con `venue_probe.in_release_window`, il :07 come controllo positivo |
| `_cron_dichiarato` accettava solo `N * * * *` e falliva come «assente» su `*/30`; `cadenza_minuti` accettava `*/45`, `0 */5`, `*/0` | si' | regex a 5 campi qualunque, regolarita' e passo > 0 imposti, parametrizzati |
| `r0823_sl_anchor.py` si rifiutava di girare nella finestra del **vecchio** `:07` e stampava a runtime che il docstring «prescrive ~230 min»; `cron_chain.sh` giustificava il :25 «fuori dal :07» | si' | guardia sul :47, prosa al passato, commento aggiornato |
+13 -6
View File
@@ -703,9 +703,16 @@ e i vincoli di deploy (PRIIPs/UCITS/broker).
movimento certo e moltiplica i segmenti (gli ambigui non spezzano: restano nel rendimento, movimento certo e moltiplica i segmenti (gli ambigui non spezzano: restano nel rendimento,
dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati, dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati,
trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Due limiti trovati in revisione (02/09):** l'intervallo che contiene un movimento certo esce INTERO dal rendimento (il suo P&L di mercato va in `certi`; errore massimo META' del movimento, per costruzione; il 25/08 ~$0,5) e a base di equity zero `twr` e `trading` sono entrambi None, perche' il salto 0→X del primo versamento e' invisibile al classificatore. I segmenti a lunghezza zero (movimento nel primo intervallo, o due consecutivi) non si stampano. **Limite ereditato:** un macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Due limiti trovati in revisione (02/09):** l'intervallo che contiene un movimento certo esce INTERO dal rendimento (il suo P&L di mercato va in `certi`; errore massimo META' del movimento, per costruzione; il 25/08 ~$0,5) e a base di equity zero `twr` e `trading` sono entrambi None, perche' il salto 0→X del primo versamento e' invisibile al classificatore. I segmenti a lunghezza zero (movimento nel primo intervallo, o due consecutivi) non si stampano. **Seconda tornata di revisione, stesso giorno:** il classificatore era **cieco ~23 ore al giorno** — il
+10% di trading fra due letture orarie consecutive, a mercato fermo, sarebbe classificato feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, e
movimento — e' il limite dichiarato del rilevatore live (D5), non uno nuovo. qualunque calo ≥10% del giorno sarebbe stato un movimento (un crash stampato come prelievo, dal
25/08); con feed assente tutto era «ambiguo» e il report stampava `e1/e0-1` sotto l'etichetta TWR.
Ora «mercato non misurabile» è uno stato: `ambiguo` con la ragione, `twr`/`trading` None con motivo.
Il «limite +10% a mercato fermo» scritto la mattina era un artefatto della fixture piatta, cancellato.
`pnl_giorno` chiama `rendimento_twr` (la frase «entrambi la chiamano» è vera solo da questa tornata),
la pagina stampa il TWR, la testata Telegram qualifica il cumulato «di cui versati», e il bound di
leva include la scala e il tetto di codice. Resta aperto (§5.15): il bound guarda solo BTC/ETH, e il
31% dell'equity è USDE — un depeg pieno sarebbe scorporato come prelievo.
**Le tre fonti si INCROCIANO e non si sovrascrivono:** log (ora vera) x jsonl (i fill) x venue **Le tre fonti si INCROCIANO e non si sovrascrivono:** log (ora vera) x jsonl (i fill) x venue
(autorevole ma **TRONCA** — 1 trade su BTC, 0 su ETH). `reconcile()` riporta le divergenze e (autorevole ma **TRONCA** — 1 trade su BTC, 0 su ETH). `reconcile()` riporta le divergenze e
**non ripara niente da solo**: fra due fonti che non concordano, una riparazione silenziosa e' **non ripara niente da solo**: fra due fonti che non concordano, una riparazione silenziosa e'
@@ -920,10 +927,10 @@ slot non è quello descritto in `venue_probe.RELEASE_*`: rileggerlo prima di spo
Il docstring di `book_execute.py` prescriveva «ogni ~230 minuti» (la griglia di SKH01) mentre il cron Il docstring di `book_execute.py` prescriveva «ogni ~230 minuti» (la griglia di SKH01) mentre il cron
gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che l'accumulo di scatti del disaster-SL gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che l'accumulo di scatti del disaster-SL
rotolante è funzione della **cadenza del cron** (1h: BTC 0 / ETH 1 in 7-8 anni · 4h: BTC 2 · 24h: ETH 5 rotolante è funzione della **cadenza del cron** (1h: BTC 0 / ETH 1 in 7-8 anni · 4h: BTC 2 · 24h: ETH 5
con 2 in 30 giorni), quindi chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro con 2 in 30 giorni), quindi chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro
sulla riga peggiore. La riparazione è il docstring stesso — cadenza ORARIA, la ragione (giro dalla riga 1h (rotolante: BTC 0 / ETH 1) alla riga 4h (BTC 2 contro 1 del pavimento; la peggiore misurata è 24h, ETH 5). La riparazione è il docstring stesso — cadenza ORARIA, la ragione (giro
idempotente: girare più fitto della griglia non costa ordini, e compra latenza ≤1h per gli idempotente: girare più fitto della griglia non costa ordini, e compra latenza ≤1h per gli
ingressi/uscite software di SKH01 e un **controllo** orario del rotolante — che si ri-ancora solo oltre la tolleranza di `ensure_disaster_sl`, mark +5,263%/4,762% o taglia >10%: la prima stesura diceva «si ri-ancora ogni ora» ed era sbagliata, corretta in revisione lo stesso giorno) e il divieto scritto — ingressi/uscite software di SKH01 e un **controllo** orario del rotolante — che si ri-ancora solo oltre la tolleranza di `ensure_disaster_sl`, mark +5,263%/4,762% o taglia >10%, quindi lo stop siede fra 33,5% e 26,5% dal mark corrente: la prima stesura diceva «si ri-ancora ogni ora» ed era sbagliata, corretta in revisione lo stesso giorno) e il divieto scritto —
più `tests/test_book_cadenza.py`, che **deriva** (P1) le tre dichiarazioni e le confronta: la riga più `tests/test_book_cadenza.py`, che **deriva** (P1) le tre dichiarazioni e le confronta: la riga
citata nel docstring, quella dichiarata nell'intestazione di `cron_book.sh` (`47 * * * *`) e la citata nel docstring, quella dichiarata nell'intestazione di `cron_book.sh` (`47 * * * *`) e la
crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il
+1 -1
View File
@@ -8,7 +8,7 @@
# MINUTO :25, e la scelta NON e' arbitraria: # MINUTO :25, e la scelta NON e' arbitraria:
# :00 cerbero-bite sparava la sua spazzata full-chain (~44 chiamate/s) e saturava il rate limit # :00 cerbero-bite sparava la sua spazzata full-chain (~44 chiamate/s) e saturava il rate limit
# Deribit per-IP — 12.186 risposte 429 in 26 ore, il 96% nel minuto tondo. # Deribit per-IP — 12.186 risposte 429 in 26 ore, il 96% nel minuto tondo.
# :07 cron_book.sh (esecuzione del book live). Il feed 5m di SKH01 passa da li': e' la cosa che # :47 cron_book.sh (esecuzione del book live; era :07 fino al 25/08). Il feed 5m di SKH01 passa da li': e' la cosa che
# NON deve trovare l'IP occupato. # NON deve trovare l'IP occupato.
# :25 nessun altro job. Il giro dura ~3 minuti a 4 chiamate/s, quindi finisce ben prima del :30. # :25 nessun altro job. Il giro dura ~3 minuti a 4 chiamate/s, quindi finisce ben prima del :30.
# #
+13 -8
View File
@@ -11,14 +11,19 @@ Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo o
data/live/book_executions.jsonl. data/live/book_executions.jsonl.
CADENZA: ORARIA `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01 CADENZA: ORARIA `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01
decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente: se non decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente; sotto
cambia nulla -> HOLD), quindi girare piu' fitto della griglia non costa ordini e compra due cose `min_order_usd` $5 -> HOLD). Girare piu' fitto della griglia costa solo MICRO-ORDINI di ri-taglia:
misurate: (1) gli ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h invece che fino a il target e' ricalcolato ogni ora sull'equity marcata, e 22 dei 48 ordini live sono |delta|<=$10
230m; (2) il disaster-SL rotolante (-30%, l'unico on-book) viene CONTROLLATO ogni ora e a segnale invariato spiccioli oggi, ma il deadband e' in valuta ASSOLUTA (regola C2: a capitale
ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl` (stop a >5% da quello piazzato, cioe' maggiore un movimento dello 0,15% lo supera ogni ora). Compra due cose misurate: (1) gli
mark +5,263%/-4,762%, o taglia >10%) con questa isteresi a cadenza 1h in 7-8 anni BTC 0 scatti / ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h CON LA FEED 5m FRESCA: se e' stantia
ETH 1, sulla riga 4h BTC ne fa 2 (`r0823_sl_anchor.py`). Lo stop siede quindi fra -26,3% e -33,3% `livefeed.fresh_5m` ripiega in silenzio sul feed giornaliero (latenza ~1 giorno) e
dal mark corrente, non a -30% dall'ultima ora: e' rotolante, non un massimo di perdita (CLAUDE.md §1). `skh_feed_max_age_min` 30 ALLERTA, non blocca; (2) il disaster-SL rotolante (-30%, l'unico on-book)
viene CONTROLLATO ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl` (stop a
>5% da quello piazzato, cioe' mark +5,263%/-4,762%, o taglia >10%) — con questa isteresi
(`r0823_sl_anchor.py`): a cadenza 1h in 7-8 anni BTC 0 scatti / ETH 1; a 4h BTC 2; a 24h ETH 5,
con 2 in 30 giorni. Lo stop siede quindi fra -33,5% e -26,5% dal mark corrente (0,665-0,735 x mark),
non a -30% dall'ultima ora: e' rotolante, non un massimo di perdita (CLAUDE.md §1).
🚨 NON "correggere" la cadenza a ~230 minuti: fino al 2026-09-02 questo docstring lo prescriveva, 🚨 NON "correggere" la cadenza a ~230 minuti: fino al 2026-09-02 questo docstring lo prescriveva,
ed era il docstring a essere sbagliato, non il cron (debito §5.7). `tests/test_book_cadenza.py` ed era il docstring a essere sbagliato, non il cron (debito §5.7). `tests/test_book_cadenza.py`
tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate
+4 -1
View File
@@ -91,7 +91,10 @@ def report() -> None:
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i" print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
f" (restano nel P&L, dichiarati)") f" (restano nel P&L, dichiarati)")
for ev in r["eventi"]: for ev in r["eventi"]:
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}]") mk = ev.get("mercato_max_pct")
dettaglio = (ev.get("mercato_nota") or "mercato non misurabile") if mk is None \
else f"mercato max {100 * mk:.2f}% a leva piena, salto {100 * ev['pct']:+.1f}%"
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}: {dettaglio}]")
if r["trading"] is None: if r["trading"] is None:
print(f" trading da arming : n/d ({r['motivo']})") print(f" trading da arming : n/d ({r['motivo']})")
else: else:
+11 -10
View File
@@ -17,7 +17,7 @@ SOLA LETTURA. Questo script:
- non importa nulla che invii ordini se non per ISPEZIONARLO (la sottoclasse di §1 ha `_post` che - non importa nulla che invii ordini se non per ISPEZIONARLO (la sottoclasse di §1 ha `_post` che
SOLLEVA: se il codice di produzione toccasse la rete, il test fallirebbe rumorosamente); SOLLEVA: se il codice di produzione toccasse la rete, il test fallirebbe rumorosamente);
- tocca il venue solo con `--venue`, solo `get_open_orders` (lettura), 2 richieste, e si rifiuta - tocca il venue solo con `--venue`, solo `get_open_orders` (lettura), 2 richieste, e si rifiuta
di partire nei minuti :05-:10 (cron_book) e :24-:30 (cron_chain). di partire nei minuti :45-:50 (cron_book, al :47 dal 25/08) e :24-:30 (cron_chain).
uv run python scripts/research/r0823_sl_anchor.py # attesa + log + simulazione uv run python scripts/research/r0823_sl_anchor.py # attesa + log + simulazione
uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture) uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture)
@@ -283,8 +283,8 @@ def riscontro_venue(runs_info: dict):
diritte di Deribit appartengono a un ALTRO progetto: usarle sarebbe inventare un percorso, e la diritte di Deribit appartengono a un ALTRO progetto: usarle sarebbe inventare un percorso, e la
domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""") domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""")
m = datetime.now(timezone.utc).minute m = datetime.now(timezone.utc).minute
if 5 <= m <= 10 or 24 <= m <= 30: if 45 <= m <= 50 or 24 <= m <= 30: # cron_book al :47 dal 2026-08-25 (era :07), cron_chain :25
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:07) / cron_chain (:25). Non interrogo.") print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:47) / cron_chain (:25). Non interrogo.")
return return
from src.live.deribit import DeribitRead from src.live.deribit import DeribitRead
r = DeribitRead() r = DeribitRead()
@@ -395,10 +395,11 @@ def conseguenza():
alta dopo una salita, lo stop siede vicino sotto il mercato, il crollo lo prende, si rientra e alta dopo una salita, lo stop siede vicino sotto il mercato, il crollo lo prende, si rientra e
la gamba successiva lo riprende. **Il rischio di accumulo e' funzione della CADENZA DEL CRON, la gamba successiva lo riprende. **Il rischio di accumulo e' funzione della CADENZA DEL CRON,
non di k**: il gradino di leva non lo crea e non lo amplifica. non di k**: il gradino di leva non lo crea e non lo amplifica.
· E qui c'e' un aggancio operativo: il docstring di `scripts/live/book_execute.py` prescrive · Aggancio operativo (RIPARATO il 2026-09-02, `tests/test_book_cadenza.py`): fino a quel
«ogni ~230 minuti» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 giorno il docstring di `scripts/live/book_execute.py` prescriveva «ogni ~230 minuti» (la
giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 giri in 1442 ore).
riga 1h alla riga 4h quella in cui BTC raddoppia gli scatti. Chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro dalla riga 1h
(rotolante: 0 scatti) alla riga 4h (2 scatti del rotolante contro 1 del pavimento).
(*) CAVEAT sulla colonna, e sul suo artefatto: in questa lente la posizione e' SEMPRE aperta (*) CAVEAT sulla colonna, e sul suo artefatto: in questa lente la posizione e' SEMPRE aperta
dal 2018, quindi in modo `roll` l'"ingresso" e' un prezzo di anni prima e il -48%/-61% e' il dal 2018, quindi in modo `roll` l'"ingresso" e' un prezzo di anni prima e il -48%/-61% e' il
@@ -501,9 +502,9 @@ def costo(out: dict):
E c'e' un accoppiamento nuovo da registrare: l'accumulo di episodi e' funzione della CADENZA E c'e' un accoppiamento nuovo da registrare: l'accumulo di episodi e' funzione della CADENZA
DEL CRON (1h: no · 4h: BTC 2 · 24h: ETH 5 con 2 in 30 giorni). Oggi la cadenza e' misurata sana, DEL CRON (1h: no · 4h: BTC 2 · 24h: ETH 5 con 2 in 30 giorni). Oggi la cadenza e' misurata sana,
ma nessun sorvegliante la controlla contro questa conseguenza, e il docstring di `book_execute` e dal 2026-09-02 `tests/test_book_cadenza.py` tiene d'accordo docstring, cron_book.sh e crontab
ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il (il docstring ne prescriveva una, ~230 min, nella riga 4h). Resta il parametro da rileggere prima
parametro da rileggere prima del gradino, insieme a G2.""") del gradino, insieme a G2.""")
def main(): def main():
+4 -1
View File
@@ -205,7 +205,10 @@ def testata(voce: dict) -> str:
r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"] r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"]
r.append(" equity " + (f"<b>${eq:,.2f}</b>" if eq is not None else "n/d") r.append(" equity " + (f"<b>${eq:,.2f}</b>" if eq is not None else "n/d")
+ ("" if dq is None else f" ({dq:+.2f} oggi)") + ("" if dq is None else f" ({dq:+.2f} oggi)")
+ ("" if cum is None else f" · dall'arming {cum:+.2f}")) + ("" if cum is None else f" · dall'arming {cum:+.2f}"
+ (f" (di cui {p['mov_arming']:+.2f} versati -> trading "
f"{p['trading_da_arming']:+.2f})"
if p.get("mov_arming") and p.get("trading_da_arming") is not None else "")))
if "errore" not in lb and lb.get("asset"): if "errore" not in lb and lb.get("asset"):
pos = " · ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted(lb["asset"].items())) pos = " · ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted(lb["asset"].items()))
r.append(f" {pos} · leva {lb.get('leva_lorda') or 0:.2f}x") r.append(f" {pos} · leva {lb.get('leva_lorda') or 0:.2f}x")
+51 -12
View File
@@ -148,30 +148,48 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia) return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
try: try:
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text()) cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
leva_tetto = float(cfg["max_notional_per_asset_frac"]) * len(ASSETS) # la leva lorda massima che il libro puo' tenere: frac x n_asset x scala, mai oltre il
# tetto di CODICE (P1: derivata da `book`, non ridichiarata — la chiave di scala del
# 01/09 entra qui da sola)
scala = float(cfg.get("book_scale_k") or 1.0)
leva_tetto = min(float(cfg["max_notional_per_asset_frac"]) * len(ASSETS) * scala,
float(_book.LEVA_LORDA_MAX))
except Exception: except Exception:
leva_tetto = 1.0 # il tetto di progetto: bound conservativo, usato solo qui leva_tetto = float(_book.LEVA_LORDA_MAX) # config illeggibile: il tetto di codice
px = {a: _px_1h(a) for a in ASSETS} px = {a: _px_1h(a) for a in ASSETS}
eventi = [] eventi = []
for prev, cur in candidati: for prev, cur in candidati:
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"]) t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
delta = float(cur["equity"] - prev["equity"]) delta = float(cur["equity"] - prev["equity"])
pct = delta / prev["equity"] pct = delta / prev["equity"]
mosse = [] # Il mercato e' MISURABILE solo se il feed copre entrambe le letture. Il feed 1h
# certificato si ferma alle 00:00 (rebuild alle 00:30): per il resto del giorno
# `asof` restituirebbe la STESSA barra per t0 e t1, max_mkt 0, e qualunque calo >=10%
# diventerebbe un "movimento" — un crash stampato come prelievo (revisione 02/09).
# Non misurabile => `ambiguo` con la ragione (P5: "non vedo" non e' "mercato fermo").
mosse, non_mis = [], []
for a in ASSETS: for a in ASSETS:
s = px.get(a) s = px.get(a)
if s is None or s.empty: if s is None or s.empty:
continue non_mis.append(f"{a}: feed 1h non leggibile"); continue
fine = s.index[-1] + pd.Timedelta(hours=1) # l'ultima barra copre un'ora
if t0 < s.index[0] or t1 > fine:
non_mis.append(f"{a}: feed 1h fermo a {s.index[-1]:%Y-%m-%dT%H:%M}Z"); continue
p0, p1 = s.asof(t0), s.asof(t1) p0, p1 = s.asof(t0), s.asof(t1)
if p0 and p1 and not (pd.isna(p0) or pd.isna(p1)): if pd.isna(p0) or pd.isna(p1) or not p0 or not p1:
mosse.append(abs(float(p1) / float(p0) - 1.0)) non_mis.append(f"{a}: barra assente"); continue
max_mkt = max(mosse) * leva_tetto if mosse else None mosse.append(abs(float(p1) / float(p0) - 1.0))
classe = "movimento" if (max_mkt is not None and abs(pct) > 2.0 * max_mkt) else "ambiguo" if non_mis:
max_mkt, classe, nota = None, "ambiguo", "mercato non misurabile: " + "; ".join(non_mis)
else:
max_mkt = max(mosse) * leva_tetto
classe = "movimento" if abs(pct) > 2.0 * max_mkt else "ambiguo"
nota = None
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"], eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
prima=float(prev["equity"]), dopo=float(cur["equity"]), prima=float(prev["equity"]), dopo=float(cur["equity"]),
delta=delta, pct=float(pct), delta=delta, pct=float(pct),
mercato_max_pct=(None if max_mkt is None else float(max_mkt)), mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
classe=classe)) mercato_nota=nota, classe=classe))
return dict(eventi=eventi, return dict(eventi=eventi,
certi=float(sum(e["delta"] for e in eventi if e["classe"] == "movimento")), certi=float(sum(e["delta"] for e in eventi if e["classe"] == "movimento")),
ambigui=float(sum(e["delta"] for e in eventi if e["classe"] == "ambiguo")), ambigui=float(sum(e["delta"] for e in eventi if e["classe"] == "ambiguo")),
@@ -200,7 +218,10 @@ def rendimento_twr(con, fino_ts: str) -> dict:
Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due
movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento. movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento.
TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Base di equity zero => TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Mercato NON misurabile
per almeno un salto (feed 1h fermo, o assente) => entrambi None: non si conosce la classe
del salto, quindi non si conosce il rendimento e `e1/e0-1` non torna sotto un'etichetta
piu' autorevole. Base di equity zero =>
entrambi None: il salto 0 -> X del primo versamento e' INVISIBILE a `movimenti_capitale` entrambi None: il salto 0 -> X del primo versamento e' INVISIBILE a `movimenti_capitale`
(una base zero non ha percentuale), quindi `certi` lo perderebbe e `trading` varrebbe (una base zero non ha percentuale), quindi `certi` lo perderebbe e `trading` varrebbe
l'intero conto — un numero etichettato "al netto dei versamenti" che li contiene tutti. l'intero conto — un numero etichettato "al netto dei versamenti" che li contiene tutti.
@@ -217,6 +238,16 @@ def rendimento_twr(con, fino_ts: str) -> dict:
motivo="base di equity nulla: il primo versamento non e' classificabile", motivo="base di equity nulla: il primo versamento non e' classificabile",
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"], segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
delta_equity=e1 - e0, letture=len(rows)) delta_equity=e1 - e0, letture=len(rows))
non_mis = [e for e in mv["eventi"] if e.get("mercato_nota")]
if non_mis:
# un salto oltre soglia che NON si e' potuto confrontare col mercato non e' ne' un
# movimento ne' un rendimento: il TWR non si stampa (altrimenti `e1/e0-1` tornerebbe
# sotto l'etichetta "TWR", il numero che il debito 14 e' nato per uccidere)
return dict(twr=None, trading=None,
motivo=(f"{len(non_mis)} salto/i non classificabile/i — "
+ non_mis[0]["mercato_nota"]),
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
delta_equity=e1 - e0, letture=len(rows))
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"] certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0 segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
for ev in certi: # gia' in ordine di tempo for ev in certi: # gia' in ordine di tempo
@@ -253,7 +284,10 @@ def pnl_giorno(con, giorno: date) -> dict:
e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone() e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone()
delta = (e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None delta = (e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None
cum = (e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None cum = (e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None
mv = movimenti_capitale(con, f"{g}T23:59:59+00:00") # il cumulato dall'arming, i movimenti e il trading al netto vengono da rendimento_twr:
# UNA funzione per i due lettori (report e giornale), non due aritmetiche (revisione 02/09)
r_twr = rendimento_twr(con, f"{g}T23:59:59+00:00")
mv = dict(eventi=r_twr["eventi"], certi=r_twr["certi"], ambigui=r_twr["ambigui"])
ev_g = [e for e in mv["eventi"] if e["ts_dopo"][:10] == g] ev_g = [e for e in mv["eventi"] if e["ts_dopo"][:10] == g]
mov_g = sum(e["delta"] for e in ev_g if e["classe"] == "movimento") mov_g = sum(e["delta"] for e in ev_g if e["classe"] == "movimento")
amb_g = sum(e["delta"] for e in ev_g if e["classe"] == "ambiguo") amb_g = sum(e["delta"] for e in ev_g if e["classe"] == "ambiguo")
@@ -267,7 +301,8 @@ def pnl_giorno(con, giorno: date) -> dict:
movimenti_giorno=ev_g, mov_giorno=float(mov_g), mov_giorno_ambiguo=float(amb_g), movimenti_giorno=ev_g, mov_giorno=float(mov_g), mov_giorno_ambiguo=float(amb_g),
trading_giorno=(delta - mov_g) if delta is not None else None, trading_giorno=(delta - mov_g) if delta is not None else None,
movimenti_arming=mv["eventi"], mov_arming=mv["certi"], mov_arming_ambiguo=mv["ambigui"], movimenti_arming=mv["eventi"], mov_arming=mv["certi"], mov_arming_ambiguo=mv["ambigui"],
trading_da_arming=(cum - mv["certi"]) if cum is not None else None) trading_da_arming=(r_twr["trading"] if cum is not None else None),
twr_da_arming=(r_twr["twr"] if cum is not None else None), twr_motivo=r_twr["motivo"])
def salute(con, giorno: date) -> dict: def salute(con, giorno: date) -> dict:
@@ -639,6 +674,10 @@ def rendi_markdown(con, voce: dict) -> str:
if p.get("mov_arming"): if p.get("mov_arming"):
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading " riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
f"**{_n(p.get('trading_da_arming'),'${:+,.2f}')}**") f"**{_n(p.get('trading_da_arming'),'${:+,.2f}')}**")
if p.get("twr_da_arming") is not None:
riga_c += f" · TWR **{100 * p['twr_da_arming']:+.2f}%**"
elif p.get("twr_motivo"):
riga_c += f" · TWR n/d ({p['twr_motivo']})"
if p.get("mov_arming_ambiguo"): if p.get("mov_arming_ambiguo"):
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti" riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
L += ["", "## P&L", "", L += ["", "## P&L", "",
+88 -33
View File
@@ -1,9 +1,10 @@
"""La cadenza del libro live e' ORARIA, e le tre dichiarazioni devono restare d'accordo. """La cadenza del libro live e' ORARIA, e le tre dichiarazioni devono restare d'accordo.
Debito §5.7 (chiuso il 2026-09-02): il docstring di `book_execute.py` prescriveva «ogni ~230 Debito §5.7 (chiuso il 2026-09-02): il docstring di `book_execute.py` prescriveva «ogni ~230
minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che sulla minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che gli
riga 4h BTC RADDOPPIA gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5). scatti del disaster-SL rotolante dipendono dalla cadenza 1h: BTC 0 / ETH 1 in 7-8 anni; 4h:
Chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore. BTC 2 (contro 1 del pavimento); 24h: ETH 5 con 2 in 30 giorni. Chi avesse "corretto" il CRON
verso il docstring avrebbe spostato il libro dalla riga 1h a quella 4h.
P1: il bersaglio si DERIVA da cio' che gira. Qui le fonti sono tre — il docstring (cio' che un P1: il bersaglio si DERIVA da cio' che gira. Qui le fonti sono tre — il docstring (cio' che un
umano legge), `cron_book.sh` (cio' che dichiara di essere installato) e la crontab (cio' che umano legge), `cron_book.sh` (cio' che dichiara di essere installato) e la crontab (cio' che
@@ -15,19 +16,30 @@ import ast
import re import re
import shutil import shutil
import subprocess import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.live import venue_probe as VP # noqa: E402 lo slot di release, dichiarato li'
from src.strategies.skyhook import LTF_MIN # noqa: E402 la griglia di SKH01, NON ridichiarata
BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py" BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py"
CRON_BOOK = ROOT / "scripts" / "cron_book.sh" CRON_BOOK = ROOT / "scripts" / "cron_book.sh"
GRIGLIA_SKH_MIN = 230 # data1 di SKH01 (src/strategies/skyhook.py): la riga da NON usare RIGA_4H_MIN = 240 # la riga in cui il rotolante di BTC passa da 0 a 2 scatti (r0823_sl_anchor.py);
RIGA_PEGGIORE_MIN = 240 # 4h: dove BTC raddoppia gli scatti (r0823_sl_anchor.py) # la PEGGIORE misurata e' 24h (ETH 5, 2 in 30 giorni), non questa
_RX_CRON5 = re.compile(r"`([\d*/,-]+(?: [\d*/,-]+){4})`")
def cadenza_minuti(expr: str) -> int: def cadenza_minuti(expr: str) -> int:
"""Minuti fra due esecuzioni di un'espressione crontab a 5 campi, per i soli casi regolari.""" """Minuti fra due esecuzioni di un'espressione crontab a 5 campi, SOLO per cadenze regolari
(intervalli tutti uguali): `47 * * * *` -> 60, `*/30 * * * *` -> 30, `7 */4 * * *` -> 240.
`*/45` (intervalli 45/15), `0 */5` (ultimo intervallo 4h) e `*/0` vengono RIFIUTATI."""
campi = expr.split() campi = expr.split()
assert len(campi) == 5, expr assert len(campi) == 5, expr
m, h, dom, mon, dow = campi m, h, dom, mon, dow = campi
@@ -35,9 +47,15 @@ def cadenza_minuti(expr: str) -> int:
if h == "*" and m.isdigit(): if h == "*" and m.isdigit():
return 60 return 60
if h == "*" and m.startswith("*/"): if h == "*" and m.startswith("*/"):
return int(m[2:]) passo = int(m[2:])
if passo <= 0 or 60 % passo:
raise ValueError(f"passo di minuti non regolare: {expr}")
return passo
if h.startswith("*/") and m.isdigit(): if h.startswith("*/") and m.isdigit():
return 60 * int(h[2:]) passo = int(h[2:])
if passo <= 0 or 24 % passo:
raise ValueError(f"passo di ore non regolare: {expr}")
return 60 * passo
raise ValueError(f"espressione non riconosciuta: {expr}") raise ValueError(f"espressione non riconosciuta: {expr}")
@@ -46,25 +64,30 @@ def _docstring() -> str:
def _cron_dichiarato() -> str: def _cron_dichiarato() -> str:
"""L'espressione crontab che `cron_book.sh` dichiara nel suo intestazione, fra backtick.""" """La prima espressione crontab a 5 campi che `cron_book.sh` dichiara fra backtick — di
m = re.search(r"`(\d+ \* \* \* \*)`", CRON_BOOK.read_text()) qualunque forma: e' `cadenza_minuti` a giudicarla, non questa regex (P4: un'espressione
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab" presente ma non oraria deve fallire come 'non oraria', non come 'assente')."""
m = _RX_CRON5.search(CRON_BOOK.read_text())
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab fra backtick"
return m.group(1) return m.group(1)
def _cron_installato() -> str | None: def _crontab() -> str | None:
"""Testo della crontab dell'utente, None se non leggibile (P5: distinto da 'vuota')."""
if not shutil.which("crontab"): if not shutil.which("crontab"):
return None return None
try: try:
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10) out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
except Exception: except Exception:
return None return None
if out.returncode != 0: return out.stdout if out.returncode == 0 else None
return None
for riga in out.stdout.splitlines():
if "cron_book.sh" in riga and not riga.lstrip().startswith("#"): def _righe_attive(testo: str, marcatore: str) -> list[str]:
return " ".join(riga.split()[:5]) """Le espressioni (5 campi) di TUTTE le righe attive che citano `marcatore`: due righe sono
return None due esecuzioni, e la seconda non deve nascondersi dietro la prima."""
return [" ".join(r.split()[:5]) for r in testo.splitlines()
if marcatore in r and not r.lstrip().startswith("#")]
# ---------------------------------------------------------------- regressione sul docstring # ---------------------------------------------------------------- regressione sul docstring
@@ -114,11 +137,11 @@ def test_il_docstring_dichiara_la_stessa_riga_del_cron():
# ---------------------------------------------------------------- cron_book.sh # ---------------------------------------------------------------- cron_book.sh
def test_cron_book_e_orario_e_sta_lontano_dalla_riga_peggiore(): def test_cron_book_e_orario_sotto_la_griglia_skh_e_sotto_la_riga_4h():
expr = _cron_dichiarato() c = cadenza_minuti(_cron_dichiarato())
c = cadenza_minuti(expr)
assert c == 60 assert c == 60
assert c < GRIGLIA_SKH_MIN < RIGA_PEGGIORE_MIN assert c < LTF_MIN, "la cadenza deve stare SOTTO la griglia di SKH01 (latenza <= 1 barra)"
assert c < RIGA_4H_MIN, "la riga 4h e' quella in cui il rotolante di BTC fa 2 scatti"
assert "ORARIA" in CRON_BOOK.read_text() assert "ORARIA" in CRON_BOOK.read_text()
@@ -128,25 +151,57 @@ def test_cron_book_evita_il_minuto_tondo():
assert minuto != 0 assert minuto != 0
def test_cron_book_evita_lo_slot_di_release_deribit():
# vincolo 2 del commento di cron_book.sh — il motivo vero del :07 -> :47 (4 giri morti in
# 63 giorni): il giro del martedi' alle 09:MM deve cadere FUORI dallo slot dichiarato in
# venue_probe (derivato, non riscritto). Il :07 lo violava; se qualcuno tornasse a :15
# con docstring e crontab coerenti, gli altri test passerebbero e questo no.
minuto = int(_cron_dichiarato().split()[0])
base = datetime(2026, 9, 1, tzinfo=timezone.utc) # un lunedi'
martedi = base + timedelta(days=(VP.RELEASE_WEEKDAY - base.weekday()) % 7)
giro = martedi.replace(hour=VP.RELEASE_START_MIN // 60, minute=minuto)
assert not VP.in_release_window(giro), f"il giro :{minuto:02d} cade nello slot di release"
assert VP.in_release_window(giro.replace(minute=7)), "controllo positivo: il :07 ci cadeva"
@pytest.mark.parametrize("expr,atteso", [("47 * * * *", 60), ("*/30 * * * *", 30), @pytest.mark.parametrize("expr,atteso", [("47 * * * *", 60), ("*/30 * * * *", 30),
("7 */4 * * *", 240), ("0 */24 * * *", 1440)]) ("7 */4 * * *", 240), ("0 */24 * * *", 1440)])
def test_il_parser_della_cadenza(expr, atteso): def test_il_parser_della_cadenza(expr, atteso):
assert cadenza_minuti(expr) == atteso assert cadenza_minuti(expr) == atteso
def test_il_parser_rifiuta_cio_che_non_sa_leggere(): @pytest.mark.parametrize("expr", ["47 9 * * 2", "*/45 * * * *", "0 */5 * * *", "*/0 * * * *",
"47 * 1 * *"])
def test_il_parser_rifiuta_cio_che_non_e_regolare(expr):
with pytest.raises((ValueError, AssertionError)): with pytest.raises((ValueError, AssertionError)):
cadenza_minuti("47 9 * * 2") cadenza_minuti(expr)
# ---------------------------------------------------------------- la crontab che GIRA # ---------------------------------------------------------------- la crontab che GIRA
def test_la_crontab_installata_e_quella_dichiarata(): def test_la_crontab_installata_e_quella_dichiarata_e_una_sola():
"""P16: il cron gira dalla macchina, non dal repo. Dove la crontab e' leggibile (la VPS), """P16: il cron gira dalla macchina, non dal repo. TRE stati, non due (P5):
deve coincidere con cio' che cron_book.sh dichiara; altrove il test si dichiara saltato, - crontab NON leggibile -> SALTATO (non verificabile qui);
non verde (P5: 'non vedo' non e' 'va tutto bene').""" - leggibile, e questo progetto non ci compare affatto -> SALTATO (macchina di sviluppo);
vivo = _cron_installato() - leggibile con altre righe del progetto ma SENZA cron_book.sh attivo -> ROSSO: il libro
if vivo is None: non e' schedulato dove il progetto e' installato (una riga commentata per manutenzione e
pytest.skip("crontab non leggibile o senza cron_book.sh: non verificabile qui") dimenticata e' esattamente questo caso).
assert vivo == _cron_dichiarato() E la riga deve essere UNA: due righe attive sono due esecuzioni l'ora."""
assert cadenza_minuti(vivo) == 60 testo = _crontab()
if testo is None:
pytest.skip("crontab non leggibile: non verificabile qui")
if str(ROOT) not in testo and ROOT.name not in testo:
pytest.skip("la crontab non cita questo progetto: non e' la macchina che lo esegue")
righe = _righe_attive(testo, "cron_book.sh")
assert righe, "questa macchina esegue il progetto ma cron_book.sh NON e' schedulato"
assert len(righe) == 1, f"cron_book.sh schedulato {len(righe)} volte: {righe}"
assert righe[0] == _cron_dichiarato()
assert cadenza_minuti(righe[0]) == 60
def test_due_righe_attive_non_si_nascondono_dietro_la_prima():
finta = ("47 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
"# 7 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
"17 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n")
assert _righe_attive(finta, "cron_book.sh") == ["47 * * * *", "17 * * * *"]
assert _righe_attive("# 47 * * * * /x/cron_book.sh\n", "cron_book.sh") == []
+58
View File
@@ -601,3 +601,61 @@ def test_twr_senza_tempo_a_mercato_e_zero_non_None(db, monkeypatch):
("2026-08-24T11:47:01+00:00", 2000.0)]) ("2026-08-24T11:47:01+00:00", 2000.0)])
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00") r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
assert r["twr"] == 0.0 and r["segmenti"] == [] and r["trading"] == pytest.approx(0.0) assert r["twr"] == 0.0 and r["segmenti"] == [] and r["trading"] == pytest.approx(0.0)
# ------------------------------------------------- mercato NON misurabile (revisione 02/09)
# Il feed 1h certificato si ferma alle 00:00 (rebuild alle 00:30): per il resto del giorno
# `asof` restituiva la STESSA barra per le due letture, max_mkt 0, e qualunque calo >=10%
# diventava un "movimento" — un crash stampato come prelievo. E con il feed assente tutto
# era "ambiguo", certi=0, e il TWR tornava a essere e1/e0-1 sotto l'etichetta sbagliata.
def test_un_salto_oltre_la_fine_del_feed_e_ambiguo_con_la_ragione(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto()) # copre fino al 27/08
_serie_equity(db, [("2026-08-28T10:47:01+00:00", 2000.0),
("2026-08-28T11:47:01+00:00", 1760.0)]) # -12% dopo la fine
p = J.pnl_giorno(db, date(2026, 8, 28))
ev = p["movimenti_giorno"]
assert ev[0]["classe"] == "ambiguo" and ev[0]["mercato_max_pct"] is None
assert "fermo a 2026-08-27" in ev[0]["mercato_nota"]
assert p["mov_giorno"] == 0.0 # NON scorporato
assert p["trading_da_arming"] is None and p["twr_da_arming"] is None
assert "non classificabile" in p["twr_motivo"]
def test_con_il_feed_assente_il_twr_e_None_non_il_grezzo(db, monkeypatch):
monkeypatch.setattr(J, "_px_1h", lambda a: None)
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 667.0),
("2026-08-25T11:47:01+00:00", 2066.0)])
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
assert r["twr"] is None and r["trading"] is None
assert "non leggibile" in r["motivo"]
assert r["eventi"][0]["classe"] == "ambiguo"
def test_il_giornale_e_il_report_usano_la_stessa_funzione(db, monkeypatch):
# P1, revisione 02/09: pnl_giorno non rifa' piu' `cum - certi`; prende trading e TWR da
# rendimento_twr, e la pagina lo stampa accanto al cumulato
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-24T10:47:01+00:00", 600.0),
("2026-08-25T10:47:01+00:00", 630.0),
("2026-08-25T11:47:01+00:00", 2030.0),
("2026-08-25T12:47:01+00:00", 2009.7)])
p = J.pnl_giorno(db, date(2026, 8, 25))
r = J.rendimento_twr(db, "2026-08-25T23:59:59+00:00")
assert p["trading_da_arming"] == pytest.approx(r["trading"])
assert p["twr_da_arming"] == pytest.approx(r["twr"]) == pytest.approx(1.05 * 0.99 - 1)
md = J.rendi_markdown(db, _voce(pnl=p, giorno="2026-08-25"))
assert "TWR **+3.95%**" in md
def test_la_leva_del_classificatore_include_la_scala_e_il_tetto_di_codice(db, monkeypatch, tmp_path):
# P1: la chiave di scala del 01/09 entra nel bound da sola; e il fallback e' il tetto di CODICE
from src.live import book as B
cfg = tmp_path / "live.json"
cfg.write_text(json.dumps({"max_notional_per_asset_frac": 0.5, "book_scale_k": 1.25}))
monkeypatch.setattr(J, "PROJECT_ROOT", tmp_path); (tmp_path / "config").mkdir()
cfg.rename(tmp_path / "config" / "live.json")
monkeypatch.setattr(J, "_px_1h", lambda a: _px_piatto())
_serie_equity(db, [("2026-08-25T10:47:01+00:00", 600.0), ("2026-08-25T11:47:01+00:00", 2000.0)])
mv = J.movimenti_capitale(db, "2026-08-25T23:59:59+00:00")
assert mv["leva_tetto"] == pytest.approx(min(0.5 * 2 * 1.25, B.LEVA_LORDA_MAX))
+16
View File
@@ -80,3 +80,19 @@ def test_il_report_a_base_zero_dice_n_d_e_non_l_intero_conto(monkeypatch, capsys
assert "trading da arming : n/d" in out and "TWR : n/d" in out assert "trading da arming : n/d" in out and "TWR : n/d" in out
assert "trading da arming : +610.00" not in out # l'intero conto NON e' "trading" assert "trading da arming : +610.00" not in out # l'intero conto NON e' "trading"
assert "(+610.00 di equity, movimenti di capitale INCLUSI)" in out # il fatto resta, etichettato assert "(+610.00 di equity, movimenti di capitale INCLUSI)" in out # il fatto resta, etichettato
def test_il_report_con_il_feed_assente_non_stampa_il_grezzo_come_TWR(monkeypatch, capsys):
# revisione 02/09: con data/raw assente (host ripristinato prima di rebuild_history) ogni
# salto era "ambiguo", certi=0, e il report stampava +243% sotto l'etichetta TWR
monkeypatch.setattr(J, "_px_1h", lambda a: None)
con = T.connect()
_prepara(con, [("2026-08-24T10:47:01+00:00", 600.0),
("2026-08-25T11:47:01+00:00", 2000.0),
("2026-08-26T10:47:01+00:00", 2020.0)])
con.close()
_carica_script().report()
out = capsys.readouterr().out
assert "TWR : n/d" in out and "trading da arming : n/d" in out
assert "% =" not in out and "+236" not in out
assert "[ambiguo: mercato non misurabile" in out