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:
@@ -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% |
|
||||
| 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 |
|
||||
| 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
|
||||
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
|
||||
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"
|
||||
mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi lo
|
||||
avesse "corretto" nel verso del cron avrebbe spostato il libro sulla riga 4h, dove BTC
|
||||
**raddoppia** gli scatti del disaster-SL (`r0823_sl_anchor.py`). Ora il docstring dichiara
|
||||
mentre il cron gira **ogni ora** (`47 * * * *`): era il docstring a essere sbagliato, e chi
|
||||
avesse "corretto" il **CRON** verso il docstring avrebbe spostato il libro dalla riga 1h (rotolante:
|
||||
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'
|
||||
**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
|
||||
revisione il 02/09) e il divieto esplicito. **`tests/test_book_cadenza.py` (10 test)** tiene
|
||||
+5,263%/−4,762% o taglia >10%, quindi lo stop siede fra **−33,5% e −26,5%** dal mark corrente —
|
||||
«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**
|
||||
(letta con `crontab -l`; dove non e' leggibile il test si dichiara SALTATO, non verde) — e
|
||||
verifica che la cadenza dichiarata sia 60 min, sotto la griglia 230m e lontana dai 240 della
|
||||
riga peggiore. Diario `2026-09-02b-debito-7-cadenza-docstring.md`.
|
||||
(letta con `crontab -l`, **tutte** le righe attive: due righe sono due esecuzioni; tre stati —
|
||||
illeggibile ⇒ SALTATO, leggibile senza questo progetto ⇒ SALTATO, leggibile col progetto ma senza
|
||||
`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
|
||||
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**
|
||||
@@ -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'**.
|
||||
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
|
||||
`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
|
||||
trading fra due letture consecutive tocca la soglia del rilevatore e, a mercato fermo, verrebbe
|
||||
classificato «movimento» — e' lo stesso limite del rilevatore live, non un difetto nuovo.
|
||||
`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
|
||||
giorno.** Il feed 1h certificato si ferma alle 00:00 (rebuild 00:30): 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»** — 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/journal.py # voce del giorno (numeri + lettura)
|
||||
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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# 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`
|
||||
(1.678 letture) e dal report rilanciato dopo la riparazione.*
|
||||
*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.*
|
||||
|
||||
## 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à
|
||||
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 |
|
||||
|---|---|---|
|
||||
@@ -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 |
|
||||
|
||||
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
|
||||
|
||||
*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.*
|
||||
|
||||
## 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
|
||||
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 |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
|
||||
## 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,665–0,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 |
|
||||
|
||||
@@ -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,
|
||||
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
|
||||
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
|
||||
+10% di trading fra due letture orarie consecutive, a mercato fermo, sarebbe classificato
|
||||
movimento — e' il limite dichiarato del rilevatore live (D5), non uno nuovo.
|
||||
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
|
||||
feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, e
|
||||
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
|
||||
(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'
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
con 2 in 30 giorni), quindi chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# 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
|
||||
# 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.
|
||||
# :25 nessun altro job. Il giro dura ~3 minuti a 4 chiamate/s, quindi finisce ben prima del :30.
|
||||
#
|
||||
|
||||
@@ -11,14 +11,19 @@ Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo o
|
||||
data/live/book_executions.jsonl.
|
||||
|
||||
⚠️ 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
|
||||
cambia nulla -> HOLD), quindi girare piu' fitto della griglia non costa ordini e compra due cose
|
||||
misurate: (1) gli ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h invece che fino a
|
||||
230m; (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 a cadenza 1h in 7-8 anni BTC 0 scatti /
|
||||
ETH 1, sulla riga 4h BTC ne fa 2 (`r0823_sl_anchor.py`). Lo stop siede quindi fra -26,3% e -33,3%
|
||||
dal mark corrente, non a -30% dall'ultima ora: e' rotolante, non un massimo di perdita (CLAUDE.md §1).
|
||||
decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente; sotto
|
||||
`min_order_usd` $5 -> HOLD). Girare piu' fitto della griglia costa solo MICRO-ORDINI di ri-taglia:
|
||||
il target e' ricalcolato ogni ora sull'equity marcata, e 22 dei 48 ordini live sono |delta|<=$10
|
||||
a segnale invariato — spiccioli oggi, ma il deadband e' in valuta ASSOLUTA (regola C2: a capitale
|
||||
maggiore un movimento dello 0,15% lo supera ogni ora). Compra due cose misurate: (1) gli
|
||||
ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h — CON LA FEED 5m FRESCA: se e' stantia
|
||||
`livefeed.fresh_5m` ripiega in silenzio sul feed giornaliero (latenza ~1 giorno) e
|
||||
`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,
|
||||
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
|
||||
|
||||
@@ -91,7 +91,10 @@ def report() -> None:
|
||||
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
|
||||
f" (restano nel P&L, dichiarati)")
|
||||
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:
|
||||
print(f" trading da arming : n/d ({r['motivo']})")
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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 --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
|
||||
domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""")
|
||||
m = datetime.now(timezone.utc).minute
|
||||
if 5 <= m <= 10 or 24 <= m <= 30:
|
||||
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:07) / cron_chain (:25). Non interrogo.")
|
||||
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 (:47) / cron_chain (:25). Non interrogo.")
|
||||
return
|
||||
from src.live.deribit import 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
|
||||
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.
|
||||
· ⚠️ E qui c'e' un aggancio operativo: il docstring di `scripts/live/book_execute.py` prescrive
|
||||
«ogni ~230 minuti» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443
|
||||
giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla
|
||||
riga 1h alla riga 4h — quella in cui BTC raddoppia gli scatti.
|
||||
· ⚠️ Aggancio operativo (RIPARATO il 2026-09-02, `tests/test_book_cadenza.py`): fino a quel
|
||||
giorno il docstring di `scripts/live/book_execute.py` prescriveva «ogni ~230 minuti» (la
|
||||
griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 giri in 1442 ore).
|
||||
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
|
||||
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
|
||||
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`
|
||||
ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il
|
||||
parametro da rileggere prima del gradino, insieme a G2.""")
|
||||
e dal 2026-09-02 `tests/test_book_cadenza.py` tiene d'accordo docstring, cron_book.sh e crontab
|
||||
(il docstring ne prescriveva una, ~230 min, nella riga 4h). Resta il parametro da rileggere prima
|
||||
del gradino, insieme a G2.""")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -205,7 +205,10 @@ def testata(voce: dict) -> str:
|
||||
r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"]
|
||||
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 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"):
|
||||
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")
|
||||
|
||||
+51
-12
@@ -148,30 +148,48 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
|
||||
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
|
||||
try:
|
||||
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:
|
||||
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}
|
||||
eventi = []
|
||||
for prev, cur in candidati:
|
||||
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
|
||||
delta = float(cur["equity"] - 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:
|
||||
s = px.get(a)
|
||||
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)
|
||||
if p0 and p1 and not (pd.isna(p0) or pd.isna(p1)):
|
||||
mosse.append(abs(float(p1) / float(p0) - 1.0))
|
||||
max_mkt = max(mosse) * leva_tetto if mosse else None
|
||||
classe = "movimento" if (max_mkt is not None and abs(pct) > 2.0 * max_mkt) else "ambiguo"
|
||||
if pd.isna(p0) or pd.isna(p1) or not p0 or not p1:
|
||||
non_mis.append(f"{a}: barra assente"); continue
|
||||
mosse.append(abs(float(p1) / float(p0) - 1.0))
|
||||
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"],
|
||||
prima=float(prev["equity"]), dopo=float(cur["equity"]),
|
||||
delta=delta, pct=float(pct),
|
||||
mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
|
||||
classe=classe))
|
||||
mercato_nota=nota, classe=classe))
|
||||
return dict(eventi=eventi,
|
||||
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")),
|
||||
@@ -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
|
||||
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`
|
||||
(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.
|
||||
@@ -217,6 +238,16 @@ def rendimento_twr(con, fino_ts: str) -> dict:
|
||||
motivo="base di equity nulla: il primo versamento non e' classificabile",
|
||||
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
|
||||
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"]
|
||||
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
|
||||
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()
|
||||
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
|
||||
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]
|
||||
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")
|
||||
@@ -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),
|
||||
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"],
|
||||
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:
|
||||
@@ -639,6 +674,10 @@ def rendi_markdown(con, voce: dict) -> str:
|
||||
if p.get("mov_arming"):
|
||||
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
|
||||
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"):
|
||||
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
|
||||
L += ["", "## P&L", "",
|
||||
|
||||
+88
-33
@@ -1,9 +1,10 @@
|
||||
"""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
|
||||
minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che sulla
|
||||
riga 4h BTC RADDOPPIA gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5).
|
||||
Chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore.
|
||||
minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che gli
|
||||
scatti del disaster-SL rotolante dipendono dalla cadenza — 1h: BTC 0 / ETH 1 in 7-8 anni; 4h:
|
||||
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
|
||||
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 shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
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"
|
||||
CRON_BOOK = ROOT / "scripts" / "cron_book.sh"
|
||||
GRIGLIA_SKH_MIN = 230 # data1 di SKH01 (src/strategies/skyhook.py): la riga da NON usare
|
||||
RIGA_PEGGIORE_MIN = 240 # 4h: dove BTC raddoppia gli scatti (r0823_sl_anchor.py)
|
||||
RIGA_4H_MIN = 240 # la riga in cui il rotolante di BTC passa da 0 a 2 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:
|
||||
"""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()
|
||||
assert len(campi) == 5, expr
|
||||
m, h, dom, mon, dow = campi
|
||||
@@ -35,9 +47,15 @@ def cadenza_minuti(expr: str) -> int:
|
||||
if h == "*" and m.isdigit():
|
||||
return 60
|
||||
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():
|
||||
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}")
|
||||
|
||||
|
||||
@@ -46,25 +64,30 @@ def _docstring() -> str:
|
||||
|
||||
|
||||
def _cron_dichiarato() -> str:
|
||||
"""L'espressione crontab che `cron_book.sh` dichiara nel suo intestazione, fra backtick."""
|
||||
m = re.search(r"`(\d+ \* \* \* \*)`", CRON_BOOK.read_text())
|
||||
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab"
|
||||
"""La prima espressione crontab a 5 campi che `cron_book.sh` dichiara fra backtick — di
|
||||
qualunque forma: e' `cadenza_minuti` a giudicarla, non questa regex (P4: un'espressione
|
||||
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)
|
||||
|
||||
|
||||
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"):
|
||||
return None
|
||||
try:
|
||||
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||
except Exception:
|
||||
return None
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
for riga in out.stdout.splitlines():
|
||||
if "cron_book.sh" in riga and not riga.lstrip().startswith("#"):
|
||||
return " ".join(riga.split()[:5])
|
||||
return None
|
||||
return out.stdout if out.returncode == 0 else None
|
||||
|
||||
|
||||
def _righe_attive(testo: str, marcatore: str) -> list[str]:
|
||||
"""Le espressioni (5 campi) di TUTTE le righe attive che citano `marcatore`: due righe sono
|
||||
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
|
||||
@@ -114,11 +137,11 @@ def test_il_docstring_dichiara_la_stessa_riga_del_cron():
|
||||
|
||||
# ---------------------------------------------------------------- cron_book.sh
|
||||
|
||||
def test_cron_book_e_orario_e_sta_lontano_dalla_riga_peggiore():
|
||||
expr = _cron_dichiarato()
|
||||
c = cadenza_minuti(expr)
|
||||
def test_cron_book_e_orario_sotto_la_griglia_skh_e_sotto_la_riga_4h():
|
||||
c = cadenza_minuti(_cron_dichiarato())
|
||||
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()
|
||||
|
||||
|
||||
@@ -128,25 +151,57 @@ def test_cron_book_evita_il_minuto_tondo():
|
||||
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),
|
||||
("7 */4 * * *", 240), ("0 */24 * * *", 1440)])
|
||||
def test_il_parser_della_cadenza(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)):
|
||||
cadenza_minuti("47 9 * * 2")
|
||||
cadenza_minuti(expr)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- la crontab che GIRA
|
||||
|
||||
def test_la_crontab_installata_e_quella_dichiarata():
|
||||
"""P16: il cron gira dalla macchina, non dal repo. Dove la crontab e' leggibile (la VPS),
|
||||
deve coincidere con cio' che cron_book.sh dichiara; altrove il test si dichiara saltato,
|
||||
non verde (P5: 'non vedo' non e' 'va tutto bene')."""
|
||||
vivo = _cron_installato()
|
||||
if vivo is None:
|
||||
pytest.skip("crontab non leggibile o senza cron_book.sh: non verificabile qui")
|
||||
assert vivo == _cron_dichiarato()
|
||||
assert cadenza_minuti(vivo) == 60
|
||||
def test_la_crontab_installata_e_quella_dichiarata_e_una_sola():
|
||||
"""P16: il cron gira dalla macchina, non dal repo. TRE stati, non due (P5):
|
||||
- crontab NON leggibile -> SALTATO (non verificabile qui);
|
||||
- leggibile, e questo progetto non ci compare affatto -> SALTATO (macchina di sviluppo);
|
||||
- leggibile con altre righe del progetto ma SENZA cron_book.sh attivo -> ROSSO: il libro
|
||||
non e' schedulato dove il progetto e' installato (una riga commentata per manutenzione e
|
||||
dimenticata e' esattamente questo caso).
|
||||
E la riga deve essere UNA: due righe attive sono due esecuzioni l'ora."""
|
||||
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") == []
|
||||
|
||||
@@ -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)])
|
||||
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)
|
||||
|
||||
|
||||
# ------------------------------------------------- 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))
|
||||
|
||||
@@ -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 : +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
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user