Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79afe41ec2 | |||
| 8f2e6c5c2c | |||
| 0a2f780d3c |
@@ -113,6 +113,16 @@ deposito va **una riga di giornale**, non ricostruita dopo. `r0901d_soldi_fermi.
|
||||
|
||||
**Candidati in forward-monitor** (nessuno nel book): XSR01 · DVOLSPREAD · STATARB · PREVDAY.
|
||||
|
||||
📌 **Revisione SETTIMANALE in sola lettura (dal 2026-09-09).** Lunedi' 06:15 UTC `scripts/cron_review.sh` →
|
||||
`scripts/live/revisione.py` → `src/live/revisione.py`: un revisore **diverso dall'autore** (`claude-fable-5-1`)
|
||||
rilegge CLAUDE.md, config, crontab, commit, `monitor_health`, 7 giorni di giornale e di diari, stato dei sorveglianti
|
||||
(~195k caratteri), e scrive `docs/revisioni/<data>.md` **firmato** + la Sintesi su Telegram. **Propone, non esegue:**
|
||||
l'unico file che scrive e' il rapporto; le decisioni vincolanti (§3) non le ripropone; un'idea «nuova» la propone solo
|
||||
citando la memoria che ha ucciso la simile. Il rapporto e' opinione di un lettore fallibile (P13): guardia sui numeri
|
||||
come `analista`. Un modello muto produce un rapporto che lo dice, e un 🚨 (P5). Deciso dall'operatore il 09/09 sera
|
||||
(«fai») dopo aver **scartato** autoregolazione dei parametri e generazione automatica di strategie: la ricerca non e'
|
||||
il vincolo binding dal 26/07, e un generatore e' una grid search senza fondo (M3).
|
||||
|
||||
---
|
||||
|
||||
## 2. I numeri da citare — e quelli da NON citare
|
||||
@@ -128,7 +138,7 @@ sono all'ancora fortunata; le stime oneste sono la **mediana della banda d'ancor
|
||||
| TP01 hold-out | 0,31 | **~+0,05** — il valore di TP01 e' il **taglio del DD ~6× vs buy&hold**, non il ritorno |
|
||||
| XS01 standalone | 1,50 / 1,71 / DD 11% | ensemble di fase **1,25 / 1,31 / 10,9%**; maxDD fuori campione **21%** |
|
||||
| SKH01 | i numeri di ammissione | de-luckato e' **il meno affidabile dei cinque** (l'ancora regala 2/3 del FULL) |
|
||||
| VRP01 | f = 1,00 | **f = 0,73** misurato sulle quote reali (IC95 [0,698; 0,780]) |
|
||||
| VRP01 | f = 1,00 | **f = 0,73** misurato sulle quote reali (IC95 [0,698; 0,780], 30/07); **0,71 [0,680; 0,756]** rigirando lo stesso `r0730` con `cblib` causale (09/09 sera, n=27, 0/27 ≥ 1) |
|
||||
| muro capitale-rendita | $272k (lordo) | **$313k** al netto di fisco **e** funding — banda **[$187k – $1,14M]** |
|
||||
| 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) |
|
||||
@@ -161,8 +171,9 @@ con le classi nell'ordine dichiarato sono 4 + 6 immune. **La leva che aumentereb
|
||||
peso di SKH01** (37,5/62,5 batte 12/12 episodi ma maxDD 12,1% vs 9,4%): vincolato (§3), gate
|
||||
`weights_tilt_null` fallisce su una condizione **in-sample** (delta −0,0026, 26/07; cache segnali ferma
|
||||
al 25/07). **Crollo CATTURATO a quote vere (1-5/06/2026, archivio bite, `r0909_crash_catturato.py`)**:
|
||||
f_net del put credit spread **0,74 = 0,714 del rally**; put δ−0,10 costa **1,92×** il modello all'ask ma
|
||||
paga come il modello nel crollo (0,94-1,04); f al peggior MTM 1,08 (il 2,58 della prima stesura era un
|
||||
f_net del put credit spread **0,76 [0,71-0,86] = 0,712 del rally** (rimisurato la sera del 09/09 con spot e DVOL
|
||||
causali, debito 18; era 0,74 = 0,714); put δ−0,10 costa **1,98×** il modello all'ask ma paga come il modello nel
|
||||
crollo (0,99-1,04); f al peggior MTM 1,08 (il 2,58 della prima stesura era un
|
||||
artefatto di quote oltre la larghezza). **NON e' il f di stress della regola del 19/06**: crollo a vol
|
||||
BASSA (picco DVOL al 29°/46° percentile), gate IV-rank di VRP01 CHIUSO, famiglia INVERSE, 4 strutture.
|
||||
Diario `2026-09-09-crolli-opzioni-monete.md`.
|
||||
@@ -435,14 +446,29 @@ d'ancora in modo diverso (nella differenza si cancella in parte, nel livello per
|
||||
`usde_convert.cuscino_richiesto_usd`, P1) ogni ora e allerta sotto zero; e la riconversione USDE→USDC che
|
||||
ripristina il cuscino e' `usde_convert --quota <minore>`, che esiste ma non e' automatica.
|
||||
|
||||
18. ⚠️ **`cblib.spot_series` + `asof(ts)` guarda UN'ORA AVANTI** (trovato in revisione il 09/09): il
|
||||
feed 1h e' etichettato all'**apertura** (la barra 20:00 chiude col 5m delle 20:55), quindi
|
||||
`S.asof(ts)` restituisce la chiusura di ts+1h. Tocca ogni numero di `cblib` che usa lo spot
|
||||
(il f 0,714 di §11 compreso; anche `ST = S.asof(exp)` e' il prezzo di un'ora DOPO la scadenza).
|
||||
Nella misura del 09/09 e' innocuo (ambo le gambe ITM) e giocava contro l'autore; `r0909_crash_catturato`
|
||||
usa `spot_causale` (indice +1h). **Non riparato in `cblib`**: cambierebbe numeri pubblicati — va
|
||||
riparato con la rimisura di §11, dichiarando lo scarto. D6, quarta occorrenza dell'etichetta.
|
||||
|
||||
18. ✅ **RIPARATO (2026-09-09 sera).** `cblib.spot_series` + `asof(ts)` guardava **un'ora avanti** (feed
|
||||
1h etichettato all'apertura: verificato, chiusura 1h a T == chiusura 5m a T+55 nel 100% delle barre)
|
||||
**e `dvol_series` fino a 24 ore avanti** (la riga DVOL del giorno D e' la chiusura delle 23:00 di D:
|
||||
verificato contro la risoluzione 1h dell'API pubblica) — seconda serie con lo stesso difetto, trovata
|
||||
riparando la prima (D6, quinta occorrenza). Ora `cblib.causale(s, cadenza)` rietichetta le chiusure
|
||||
all'istante in cui sono note e le due serie escono **gia' causali** (spot +1h, DVOL +1 giorno);
|
||||
`r0909` non ri-sposta. **§11 rimisurato allo stesso taglio (22/08, n=19)**: prima riprodotto al
|
||||
millesimo su worktree HEAD (0,714 [0,690-0,779]), poi **0,712 [0,664-0,732]** — le due correzioni
|
||||
hanno **segno opposto** (solo spot 0,721 · solo DVOL 0,693 · entrambe 0,712; M26) e un DVOL **orario**
|
||||
di controllo da' 0,717. Il verdetto di §11 non cambia; cambiano i numeri di contorno (Sharpe canonico
|
||||
BTC 32,58 → 29,61, mediana onesta 1,45 → **2,12**; ETH 3,90 → 7,64; fee 1,44×/1,80× → 1,51×/1,87×;
|
||||
il regolamento e' ora il prezzo delle 08:00, non delle 09:00: P&L +3,3%). Sul crollo (§75): ETH pre
|
||||
0,74 → **0,76 [0,71-0,86]**, BTC durante 0,79 → **0,89** (DVOL orario 0,83: nel crollo il DVOL
|
||||
giornaliero stantio vale −0,04/+0,06 di f). `vrp_f_watch` (cron): f canonico 0,732 → **0,706**, differenza
|
||||
col candidato +0,054 → **+0,097 [+0,042, +0,140]** — ora esclude lo zero e il criterio (b) e'
|
||||
soddisfatto, (a) no (28/40). ⚠️ **Resta aperto, con costo misurato**: il DVOL nel feed e' giornaliero,
|
||||
quindi causale = **stantio fino a 23 ore** (mediana 0,2 pt di vol, max 3 pt; nel crollo 0,06 di f):
|
||||
la via giusta e' un feed DVOL **orario** (API pubblica, gratuito, `resolution=3600`) con la sua riga
|
||||
in `cron_daily` — non fatto, e' una decisione sul feed; e lo spot causale resta **stantio ≤25 min** (snapshot
|
||||
al :25, chiusura al :00; il feed 5m certificato lo ridurrebbe a ≤5). Trovati in revisione altri due lettori:
|
||||
**`r0730_vrp_real_quotes`** (il f 0,73 di §2 → **0,71**, dichiarato in §2) e **`r0901_btc_collar`** (join
|
||||
per giorno: riallineato con `causale(·, "-1D")`, bit-exact a prima). `options_vrp_calibrate.spot_series`
|
||||
(copia propria, numeri del 20/06) **non toccato**. Diario `2026-09-09b-debito-18-cblib-causale.md`.
|
||||
---
|
||||
|
||||
## 6. Le regole di prim'ordine
|
||||
@@ -629,7 +655,8 @@ src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals
|
||||
src/live/ → book.py (esecutore netto TP01+SKH01), shadow.py, deribit.py,
|
||||
livefeed.py, venue_watch.py, monitor_health.py, notifier.py,
|
||||
scale_watch.py (chiave di scala: 3 domande, 3 stati),
|
||||
tradesdb.py · journal.py · analista.py (libro di bordo)
|
||||
tradesdb.py · journal.py · analista.py (libro di bordo),
|
||||
revisione.py (revisione settimanale, sola lettura, revisore fable)
|
||||
src/data/eq_splits.py, eq_crosscheck.py → guardie sul feed equity
|
||||
scripts/research/ → ricerca (r<data>_*.py). Harness condiviso: alt/altlib.py
|
||||
scripts/analysis/ → SOLO tool sui dati certificati: rebuild_history, certify_feed,
|
||||
@@ -637,11 +664,12 @@ scripts/analysis/ → SOLO tool sui dati certificati: rebuild_history
|
||||
scripts/live/ → book_execute · venue_watch · fee_watch · edge_watch ·
|
||||
monitor_health · collect_chain · paper_* · trades_db ·
|
||||
journal · analista
|
||||
scripts/cron_{book,daily,chain,opt_snapshot,vol_term}.sh
|
||||
scripts/cron_{book,daily,chain,opt_snapshot,vol_term,review}.sh
|
||||
docs/memory/ → LA MEMORIA (indice in testa a questo file)
|
||||
docs/research/ → BRIEF-0822 · RESULTS-0822 (§1-77) · SPEC-scale-key
|
||||
docs/diary/YYYY-MM-DD.md → una voce per esperimento (146)
|
||||
docs/journal/YYYY-MM-DD.md → libro di bordo, una voce al giorno, 4 livelli per provenienza
|
||||
docs/revisioni/YYYY-MM-DD.md → revisione settimanale firmata (lunedi'), opinione del revisore
|
||||
data/live/trades.db → i trade allineati col tempo (dentro il perimetro di backup)
|
||||
Old/ → ARCHIVIO pre-reset
|
||||
VERSION → semver
|
||||
@@ -660,7 +688,8 @@ uv run python scripts/live/<script>.py --help # uso e flag (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 (1008, tutti verdi al 09/09)
|
||||
uv run python scripts/live/revisione.py --secco # taglia del materiale della revisione settimanale (niente modello)
|
||||
uv run pytest # test (1031, tutti verdi al 09/09 sera)
|
||||
```
|
||||
|
||||
```python
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# 2026-09-09b — Debito §5.18 riparato: `cblib` causale (spot +1h, DVOL +1 giorno) e §11 rimisurato
|
||||
|
||||
**Richiesta dell'operatore:** *"ripara il debito §5.18 su cblib e rimisura §11"*. Revisione col modello
|
||||
`fable` (agente fresco), come da regola.
|
||||
|
||||
**Esito in una riga:** riparato in `cblib` — spot e DVOL escono **gia' causali** — e §11 rimisurato allo
|
||||
stesso taglio: **0,714 [0,690-0,779] → 0,712 [0,664-0,732]** (n=19). Il numero non si muove perche' le due
|
||||
correzioni hanno **segno opposto**; **nessun verdetto cambia**; riparando la prima serie ne e' uscita una
|
||||
**seconda con lo stesso difetto** (il DVOL giornaliero, fino a 24 ore avanti); il DVOL orario resta un
|
||||
passo aperto **con costo misurato**. Libro, config, cron: invariati. Nessun ordine.
|
||||
|
||||
## 0. Il difetto, verificato prima di ripararlo (D6: si controlla col lag, non si assume)
|
||||
|
||||
| serie | come e' etichettata | prova | look-ahead di `asof(ts)` |
|
||||
|---|---|---|---|
|
||||
| feed 1h certificato | all'**apertura**: la barra T contiene la chiusura di T+1h | chiusura 1h a T == chiusura 5m a **T+55** nel **100%** delle barre (BTC ed ETH, 1-5/06); a T−5 nell'1,7% / 0% | **+1 ora** |
|
||||
| `dvol_{asset}.parquet` (giornaliero, `fetch_dvol`, `resolution=86400`) | al giorno D contiene la **chiusura delle 23:00 di D** | API pubblica a risoluzione 1h, 1-4/06: `close` 1D del giorno D == `close` 1h delle 23:00 di D, e `open` 1D di D+1 == `close` di D | **fino a +24 ore** |
|
||||
|
||||
La seconda riga non era nel debito: e' uscita controllando la convenzione dell'altra serie di
|
||||
contesto che `cblib` legge con lo stesso `asof`. Il sleeve (`_vrp_weekly_asset`) **non ha il difetto**:
|
||||
lavora a cadenza giornaliera e accosta la chiusura del giorno D al DVOL del giorno D, che a quella
|
||||
cadenza e' coerente. Il difetto e' solo di chi entra **a un'ora** con una serie giornaliera.
|
||||
|
||||
## 1. La riparazione
|
||||
|
||||
`cblib.causale(s, cadenza)` sposta le **etichette** (mai i valori) dall'apertura all'istante in cui la
|
||||
chiusura e' nota. `spot_series` → `causale(·, "1h")`, `dvol_series` → `causale(·, "1D")`. Conseguenze:
|
||||
|
||||
- `asof(ts)` = ultima chiusura **conosciuta** a ts (prima della prima chiusura nota: NaN, non un valore).
|
||||
- il regolamento `ST = S.asof(exp)` alle 08:00 e' ora la chiusura della barra 07:00, cioe' **il prezzo
|
||||
delle 08:00** — prima era la chiusura della barra 08:00, cioe' il prezzo delle **09:00**, un'ora dopo la
|
||||
scadenza. Deribit regola su una TWAP dei 30 minuti prima delle 08:00: l'approssimazione e' ora dalla
|
||||
parte giusta dell'ora.
|
||||
- `r0909_crash_catturato` **non ri-sposta** (lo `spot_causale` locale della mattina e' rimosso: applicato
|
||||
due volte darebbe la chiusura di **due** ore prima — lo stesso errore col segno opposto). Test di
|
||||
non-regressione sul sorgente.
|
||||
- chi usa `cblib`: `r0822_vrp_real_quotes` (§11), `r0822_skew` (§5), `r0909` (§75), **`vrp_f_watch`**
|
||||
(cron giornaliero, `data/vrp_f_watch/state.json`), e — trovati dal revisore, non da me —
|
||||
**`r0730_vrp_real_quotes`** (il f 0,73 [0,706-0,805] di CLAUDE.md §2: rigirato stampa **0,71
|
||||
[0,680-0,756]**, n=27, 0/27 ≥ 1, f gamba lunga 1,66 → 1,49; verdetto invariato, numero dichiarato in §2)
|
||||
e **`r0901_btc_collar`** (join del DVOL **per giorno**: con la serie causale avrebbe accostato l'IV di D
|
||||
al DVOL di D−1 — riallineato con `causale(·, "-1D")`, bit-exact a prima, §71 non si muove).
|
||||
`options_vrp_calibrate.py` ha una **copia propria** di `spot_series` (numeri del 20/06): **non toccato**,
|
||||
dichiarato.
|
||||
- la sezione di REGIME di `r0822` (date di calendario: «ultimo giorno aperto», IV-rank per anno) e la
|
||||
tabella di contesto di `r0909` usano `causale(V, "-1D")`: li' serve il DVOL **del** giorno, non
|
||||
l'etichetta causale — altrimenti ogni data stampata slitta di un giorno (trovato dal revisore).
|
||||
- `r0822_vrp_real_quotes` ha ora il flag `--al <data>` (taglio delle scadenze) per riprodurre una misura
|
||||
a una data: senza, "riprodurre il 22/08" era impossibile perche' la catena cresce ogni ora.
|
||||
|
||||
## 2. Prima di pubblicare il numero nuovo, la macchina ha riprodotto quello vecchio (M23)
|
||||
|
||||
Worktree di HEAD (`git worktree add`, `data/raw` in symlink), stesso script con `--al 2026-08-22`:
|
||||
**pooled 0,714 (IC95 [0,690, 0,779], n=19)** — al millesimo, IC compreso. Poi lo stesso comando sul
|
||||
codice riparato.
|
||||
|
||||
## 3. Scomposizione: due correzioni di segno opposto (M26) e un controllo orario
|
||||
|
||||
| taglio | spot | DVOL | pooled f | IC95 | n |
|
||||
|---|---|---|---|---|---|
|
||||
| 22/08 | grezzo | grezzo (com'era) | **0,714** | [0,690, 0,779] | 19 |
|
||||
| 22/08 | causale | grezzo | 0,721 | [0,685, 0,758] | 19 |
|
||||
| 22/08 | grezzo | causale (giornaliero) | 0,693 | [0,670, 0,748] | 19 |
|
||||
| 22/08 | causale | causale (giornaliero) = **`cblib` oggi** | **0,712** | [0,664, 0,732] | 19 |
|
||||
| 22/08 | causale | causale **orario** (controllo, API pubblica) | 0,717 | [0,667, 0,732] | 19 |
|
||||
| 09/09 | grezzo | grezzo | 0,720 | [0,717, 0,952] | 23 |
|
||||
| 09/09 | causale | causale (giornaliero) | **0,712** | [0,677, 0,741] | 23 |
|
||||
| 09/09 | causale | causale orario | 0,726 | [0,680, 0,738] | 23 |
|
||||
|
||||
Il DVOL di fine giornata **alzava** f: nel rally la vol scende dentro il giorno, la chiusura di D sta
|
||||
**sotto** quella di D−1 (+1,05 pt di vol mediana su 19/19 trade, misura del revisore) → denominatore piu'
|
||||
basso → f piu' alto; causale: −0,021. Lo spot di un'ora dopo, invece, **non e' un meccanismo ma rumore**:
|
||||
lo scarto per trade ha segno misto (mediana −0,02% BTC / −0,11% ETH, banda −0,6…+0,9%) e sposta f fino a
|
||||
**±0,15 per trade** (lo spread δ−0,28/−0,10 ha delta netto ~0,18: 0,5% di spot vale 15-20% del credito
|
||||
modellato); il +0,007 netto e' la somma di 19 segni. ⚠️ Corollario: lo spot causale resta **stantio fino
|
||||
a 25 minuti** (snapshot al :25, chiusura nota al :00), cioe' ±0,05-0,10 di f per osservazione; il feed
|
||||
**5m certificato** lo porterebbe a ≤5 minuti. Non fatto: dichiarato in §6. Il DVOL giornaliero causale e' **stantio fino a
|
||||
23 ore** (agli ingressi delle 08:00: 8 ore): scarto mediano dall'orario **0,2 punti di vol**, max 1,1
|
||||
(22/08) / 3,0 (oggi). Su §11 vale 0,005 di f.
|
||||
|
||||
## 4. Cosa cambia in §11 (taglio 22/08, stesso campione)
|
||||
|
||||
| grandezza | prima | dopo | perche' |
|
||||
|---|---|---|---|
|
||||
| f pooled | 0,714 [0,690-0,779] | **0,712 [0,664-0,732]** | §3 |
|
||||
| f BTC / ETH | 0,706 / 0,715 | 0,720 / 0,692 | |
|
||||
| Sharpe canonico BTC (titolo, non citabile) | 32,58 | 29,61 | regolamento alle 08:00 |
|
||||
| **mediana onesta** BTC (9 ancore) | 1,45 | **2,12** | banda [−0,19, 29,61] (era [−0,35, 32,58]) |
|
||||
| Sharpe canonico ETH → mediana onesta | 3,90 → 2,41 | 7,64 → 3,35 | una settimana ETH cambia taglia: 26/06 −27,38 → −12,84 $ (BTC: 31/07 19,88 → 31,69, 14/08 12,94 → 8,68) |
|
||||
| media BTC fra le ancore | −1,35% → +11,22% | −0,66% → +11,44% | **cambia ancora segno**; sd 11,1× → 9,2× |
|
||||
| forfait fee vs listino | 1,44× / 1,80× | 1,51× / 1,87× | credito modellato a DVOL causale |
|
||||
| peggior mark: mediana / minimo / minimo fra le vincenti | −6,4% / −91,0% / −56,9% | −6,4% / −91,5% / −55,2% | |
|
||||
| P&L somma 19 settimane | $2.733 | $2.822 (+3,3%) | tutto dal regolamento |
|
||||
| sottostante nel campione | +25,1% / +44,2% | +21,1% / +40,1% | il vecchio regolamento del 21/08 era il prezzo delle **09:00**, che sta **+3,9%** sopra quello delle 08:00 (76.311 → 79.316): un numero a due estremi pesa un'ora intera. (La prima stesura diceva «−1,8%»: avevo letto l'ora dopo, 09:00 → 10:00. Corretto dal revisore) |
|
||||
|
||||
**Verdetto di §11: SCARTATO, per le stesse quattro ragioni** (il campione non contiene la strategia; il
|
||||
titolo non sopravvive all'ora d'ingresso; la famiglia con storia non e' quella eseguibile; lo spread e'
|
||||
il costo dominante). Nessuna delle quattro dipende da un'ora di spot. Il testo del verdetto nello script
|
||||
riporta i numeri nuovi con i vecchi accanto.
|
||||
|
||||
## 5. Effetti collaterali, misurati (tutto cio' che legge `cblib`)
|
||||
|
||||
**§75, crollo catturato** (`r0909`, rigirato): ETH pre-crollo f_net **0,74 [0,65-0,82] → 0,76
|
||||
[0,71-0,86]**; peggior MTM 1,08 [1,07-1,31] → 1,08 [1,06-1,27]; put δ−0,10 1,92× → 1,98×; al picco 0,94 →
|
||||
0,99; BTC durante **0,79 → 0,89 [0,74-1,05]**, ETH durante 0,71 → 0,78. Con il DVOL **orario** BTC durante fa **0,83**: nel crollo
|
||||
il DVOL sale di 5-6 punti al giorno, quindi il giornaliero di fine giornata (com'era) sottostima f e
|
||||
quello del giorno prima (causale) lo sovrastima — **−0,04/+0,06 di f e' il prezzo della stantiezza in un
|
||||
crollo**, ed e' l'argomento per il feed orario. DVOL "pre" 37,3 → **36,4** (BTC): il 37,3 era la chiusura
|
||||
del **primo giorno di crollo** (01/06, −3,5%), non del giorno prima — IV-rank pre 0,05 → 0,03. Picco e
|
||||
percentili **invariati** (49,1 / 69,2; 29° / 46°) dopo aver corretto la tabella di contesto (§7).
|
||||
Verdetto invariato.
|
||||
|
||||
**§5, skew** (`r0822_skew`, rigirato, 66 s): la scomposizione sul panel si muove di **≤0,005** per fattore
|
||||
(term 0,880 · skew 0,917 · spread 0,910 · fit 0,995 · f_tot 0,729); Q1/Q2 (lag prezzo/skew) **non si
|
||||
toccano** (serie proprie ancorate a `ts_max`). Agli ingressi il controllo `f_markfit` passa da 1,045 a
|
||||
**0,991** e le osservazioni che lo superano da 22/28 a **25/28**: l'ora di spot in piu' stava **nel
|
||||
controllo**, non nell'effetto — riparare ha reso il controllo piu' pulito, non il risultato diverso.
|
||||
|
||||
**`vrp_f_watch`** (sorvegliante in `cron_daily`, criteri pre-registrati il 30/07 e **non toccati**):
|
||||
|
||||
| | prima (state.json del 09/09 mattina) | dopo |
|
||||
|---|---|---|
|
||||
| coppie | 28 | 28 |
|
||||
| f canonico | 0,732 [0,702-0,784] | **0,706 [0,678-0,745]** |
|
||||
| f candidato | 0,803 [0,741-0,842] | 0,805 [0,771-0,840] |
|
||||
| differenza | +0,054 [−0,027, +0,105], 18/28 > 0 | **+0,097 [+0,042, +0,140]**, 21/28 > 0 |
|
||||
| ampiezza IC95 (b ≤ 0,12) | 0,131 — no | **0,098 — OK** |
|
||||
| coppie (a ≥ 40) | no | no |
|
||||
| `ready` | False | False |
|
||||
|
||||
Lo `state.json` e' stato **riscritto** dal mio lancio (21:14Z, senza `--quiet`; nessun Telegram: notifica
|
||||
solo a `ready`); il «prima» completo e' la colonna qui sopra (n_positive 18, `esclude_zero` False), e il
|
||||
cron lo rigenera domani. La differenza ora **esclude lo zero**. Non decide niente: (a) non e' soddisfatto e il candidato resta
|
||||
bocciato sul deflated-Sharpe, che non dipende da f. Si registra perche' fra un mese qualcuno leggera'
|
||||
"esclude lo zero" e deve sapere da quando e perche'.
|
||||
|
||||
## 6. Cosa resta aperto, col suo costo
|
||||
|
||||
| voce | costo di non farlo | costo di farlo |
|
||||
|---|---|---|
|
||||
| **DVOL orario** nel feed (`fetch_dvol --res 3600`, riga in `cron_daily`, `cblib` che lo legge) | su §11 0,005 di f; **su un crollo ±0,05**; ogni misura oraria su opzioni a DVOL giornaliero eredita lo scarto | un fetch pubblico gratuito (3.163 barre da maggio in 2 chiamate), una riga di cron, e una decisione: e' un feed nuovo dentro il perimetro (D1, certificazione, backup) |
|
||||
| **spot causale stantio ≤25 min** (snapshot :25, chiusura :00): ±0,05-0,10 di f per osservazione | `cblib.spot_series` sul feed **5m** certificato (`causale(·, "5m")`): cambia di nuovo ogni numero di `cblib`, quindi con rimisura dichiarata |
|
||||
| `options_vrp_calibrate.spot_series` (copia, 20/06) | i numeri del 20/06 (f 0,73) restano con l'ora in piu'; §11 li ha gia' sostituiti | riscriverlo su `cblib` |
|
||||
| la tabella pubblicata di §5 (x0,869 / x0,904 / x0,920) | ≤0,005 per fattore: sotto la risoluzione del campione (14+14 scadenze) | nulla: nota aggiunta |
|
||||
|
||||
## 7. Errori dell'autore in sessione
|
||||
|
||||
- **`asi8` in microsecondi** scrivendo il test della causalita' (pandas 3): i timestamp sintetici finivano
|
||||
nel 1970 e il test falliva per la fixture, non per il codice. E' la trappola del debito 1, ripagata
|
||||
scrivendo un test *sul* debito 18. Corretto con `(idx − epoch) // Timedelta(ms)`, la convenzione di
|
||||
`load_tf`.
|
||||
- **La tabella di contesto di `r0909` si e' spostata di un giorno** dopo la riparazione: mostrava il DVOL
|
||||
del giorno D−1 accanto al prezzo del giorno D, e il picco del crollo cambiava (69,2 → 68,3, 46° → 43°)
|
||||
non per una misura ma per un'etichetta. Trovato **col diff degli output**, non da un test: una tabella
|
||||
per umani non ha test. Ora il contesto usa la chiusura *del* giorno (`causale(V, "-1D")`, dichiarato
|
||||
nel commento) e `dvol_pre` / `ivr_pre` restano causali.
|
||||
- Il verdetto §7 di `r0822_vrp_real_quotes` era **prosa con numeri cablati**: sarebbe rimasto a 0,714 per
|
||||
sempre (N11 al contrario). Aggiornato con i vecchi accanto; il difetto di forma resta (i numeri del
|
||||
verdetto andrebbero calcolati, non scritti).
|
||||
|
||||
## 8. Revisione (`fable`, agente fresco) — 16 segnalazioni, verdetto «correggi e committa»
|
||||
|
||||
Ha **riverificato in proprio** la convenzione delle due serie (1h vs 5m su 120 barre: 100%; DVOL contro
|
||||
l'API oraria) e riprodotto ogni numero dei documenti lanciando gli script. Applicate: **#2** «−1,8% in
|
||||
un'ora» era falso (avevo letto 09:00 → 10:00; il fatto e' +3,9% 08:00 → 09:00) · **#3** due delle tre
|
||||
settimane «ETH» erano BTC · **#4** la parentesi sul DVOL era invertita (la chiusura di D e' piu' BASSA,
|
||||
non piu' alta) · **#5** lo spot non e' un meccanismo ma rumore, e resta stantio ≤25 min · **#6/#7** due
|
||||
lettori di `cblib` non dichiarati: `r0730` (0,73 → 0,71, ora in CLAUDE.md §2) e `r0901` (riallineato) ·
|
||||
**#8** le date di calendario di `r0822` §1 slittavano di un giorno · **#9** il test anti-doppio-shift non
|
||||
catturava `causale(spot_series, "1h")`: ora conta i `causale(` · **#10** ancora 0,74 → 0,76 nel test
|
||||
d'integrazione · **#11** teste di §75 e memoria 20 §5 con i numeri vecchi come correnti (M28) · **#12**
|
||||
«±0,05» → «−0,04/+0,06» · **#13** `state.json` riscritto: dichiarato · **#15** lo skew agli ingressi cambia
|
||||
piu' del panel (correlazioni che cambiano segno): dichiarato in §5. Non applicate: nessuna. Confermati
|
||||
senza rilievo (#1, #14, #16): verso e taglia degli shift, nessun doppio spostamento, `ST` alle 08:00
|
||||
coerente con la TWAP 07:30-08:00, tutti i numeri di §3-§5.
|
||||
|
||||
## 9. Test
|
||||
|
||||
`tests/test_cb_chain_vrp.py`: +3 (causale; `spot_series` causale sul feed 1h con controllo positivo sulla
|
||||
serie grezza; `dvol_series` causale sul giornaliero). `tests/test_r0909_crash_catturato.py`: il test dello
|
||||
shift locale diventa "lo script usa le serie di `cblib` senza ri-spostarle" (sorgente + controllo positivo
|
||||
su `cblib.causale`). Suite intera: **1011 passati, 0 falliti** (erano 1008), rilanciata dopo le correzioni della revisione: 1011/1011.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 2026-09-09c — Revisione settimanale in sola lettura: cosa si costruisce e cosa no
|
||||
|
||||
**Richiesta dell'operatore:** *«schedulare ogni x tempo un processo di auto revisione dei dati e della
|
||||
configurazione del sistema in modo che migliori con il tempo … autoregolazione e autoaggiornamento dei
|
||||
sistemi o anche creare nuovi sistemi … valutando dati macroeconomici … e poi informa me tramite
|
||||
Telegram»*. Dopo la discussione: *«fai»*.
|
||||
|
||||
**Esito in una riga:** costruita la **rilettura settimanale in sola lettura** con revisore diverso
|
||||
dall'autore (`claude-fable-5-1`), rapporto firmato in `docs/revisioni/` e Sintesi su Telegram, lunedi'
|
||||
06:15 UTC; **scartati** — con la memoria del progetto, non per prudenza generica — autoregolazione dei
|
||||
parametri, generazione automatica di strategie e macro da internet. Libro, config, pesi: invariati.
|
||||
|
||||
## 1. Perche' tre pezzi su quattro sono no
|
||||
|
||||
| pezzo | cosa dice la memoria | esito |
|
||||
|---|---|---|
|
||||
| auto-revisione periodica | nove sorveglianti, ognuno sulla sua grandezza; nessuno rilegge l'insieme; la revisione col secondo modello ha trovato oggi due conclusioni false e un look-ahead | **si'** |
|
||||
| autoregolazione dei parametri | TP01/SKH01 congelati (M20), 75/25 confermato 3 volte, `weights_tilt_null` fallisce; `scale_watch` esiste per rendere visibile ogni cambio di config; A8: una riga di config + una di giornale, decisione dell'operatore | **no** |
|
||||
| generazione e test di strategie nuove | 69 filoni in 6 ondate; ogni candidato e' un trial (M3) e deflaziona tutto; la ricerca non e' il vincolo binding dal 26/07 (+0,046 €/g il miglior lead contro +4,07%/anno per €100/mese) | **no, salvo freno**: prima si apre la memoria e si batte il motivo della morte; i candidati vanno in forward-monitor con gate e data, mai nel book |
|
||||
| dati macro da internet | gate macro / DVOL direzionale / skew de-risk: HEDGE o ridondanti (TP01 gia' flat nei crolli); un dato dal web non e' certificato (D1-D4; la APR USDC 3,40% pubblicata e mai incassata) | **no** |
|
||||
|
||||
Il centro dell'argomento: **il progetto ha gia' misurato che piu' ricerca non sposta il traguardo, i
|
||||
bonifici si'**. Un ciclo automatico che cerca strategie ottimizza la leva sbagliata e paga in trial bruciati.
|
||||
|
||||
## 2. Cosa gira
|
||||
|
||||
`scripts/cron_review.sh` (lunedi' **06:15 UTC**: fuori dal :00, fuori dal martedi' 09:00, dopo il
|
||||
`cron_daily` delle 00:30 cosi' il giornale di domenica e' chiuso) → `scripts/live/revisione.py --quiet`
|
||||
→ `src/live/revisione.scrivi`. Riga installata in crontab (`15 6 * * 1`), letta dal test.
|
||||
|
||||
**Materiale** (sola lettura, ~195k caratteri, 27 voci): CLAUDE.md intero · `config/live.json` ·
|
||||
`crontab -l` · `git log` 14 g + `git status` · `monitor_health` · stato dei sorveglianti (scale, vrp_f,
|
||||
usde, balance, venue_news, movimenti dichiarati, watermark) · 7 pagine di giornale · diari della
|
||||
settimana. Gli stati stanno **prima** delle voci grandi: alla prima stesura il tetto totale (320k) aveva
|
||||
tagliato `usde_convert.jsonl` a 2 caratteri — un sorvegliante invisibile per un limite di lunghezza.
|
||||
|
||||
**Prompt**: dieci regole vincolanti (sola lettura · ogni segnalazione cita la fonte · solo numeri del
|
||||
materiale · le decisioni di §3 non si ripropongono se la colonna «cosa la riapre» non e' soddisfatta ·
|
||||
un'idea nuova solo citando la memoria che ha ucciso la simile · i gate con data e criterio, senza
|
||||
anticipare · misurato ≠ dedotto · niente previsioni · URGENTE per cio' che serve all'operatore subito ·
|
||||
cinque titoli fissi, ~1.000 parole). L'ultimo titolo, «Cosa ho letto e cosa mi mancava», e' l'elenco da
|
||||
cui il materiale della settimana dopo migliora: e' l'unico «miglioramento col tempo» che questo sistema
|
||||
si concede, e passa da una modifica al codice, cioe' da una revisione.
|
||||
|
||||
**Uscita**: `docs/revisioni/<data>.md` firmato (modello, ora, «SOLA LETTURA», «lettore fallibile P13»,
|
||||
guardia sui numeri come `analista` con soglia 12, tabella del materiale letto). Telegram: la sola
|
||||
Sintesi, taglio dichiarato sotto i 4.096. Modello muto o risposta senza i titoli → rapporto «NON
|
||||
eseguita» col motivo e 🚨: il silenzio non e' una revisione (P5).
|
||||
|
||||
**Cosa non puo' fare, per costruzione**: scrivere altro che il rapporto (test sull'albero dei file:
|
||||
`--secco` non scrive niente, un giro scrive UN file), eseguire proposte, cambiare config. Il modello e'
|
||||
`claude-fable-5-1` per costante, e un test verifica che sia diverso da `analista.MODELLO_DEFAULT`.
|
||||
|
||||
## 3. Test — `tests/test_revisione.py` (16)
|
||||
|
||||
Finestra del materiale e file assenti dichiarati · ordine stati-prima-delle-voci-grandi · troncamento
|
||||
dichiarato · regole nel prompt · guardia (vuoto, titoli mancanti, numeri fuori con controllo positivo) ·
|
||||
rapporto firmato · modello muto → rapporto + 🚨 · Telegram con HTML neutralizzato e taglio dichiarato ·
|
||||
`--secco` non chiama e non scrive · un giro scrive un solo file · `--no-telegram` · revisore ≠ autore ·
|
||||
cadenza: il `.sh` dichiara «lunedi' HH:MM UTC», minuto ≠ 0, fuori dallo slot di release, e la crontab
|
||||
installata ha gli stessi cinque campi (skip se illeggibile). `test_cli_flag` ha preso lo script da solo
|
||||
(guardia `valida`, USO coi flag, flag del cron accettati).
|
||||
|
||||
## 4. Errori in sessione
|
||||
|
||||
- `_tronca(testo, n=MAX_CHARS_VOCE)`: il default e' catturato alla definizione, il test che patchava la
|
||||
costante non vedeva niente — la stessa trappola del debito 12 (`connect`). Letto a runtime.
|
||||
- Il tetto totale tagliava in coda: i sorveglianti erano gli ultimi e i primi a sparire. Riordinato, e
|
||||
il test lo presidia.
|
||||
|
||||
## 5. Primo giro reale (22:03Z) — e i tre difetti che solo il giro reale poteva trovare
|
||||
|
||||
Il primo lancio e' **morto prima di chiamare il modello**: `OSError: Argument list too long` — 195k
|
||||
caratteri di prompt passati come argomento alla CLI superano il limite del sistema, e `analista.interroga`
|
||||
non lo catturava: zero rapporto, zero Telegram, **il silenzio che il modulo esiste per vietare**. Nessun
|
||||
test l'avrebbe trovato (i test iniettano `interroga`): e' un difetto di trasporto, e il trasporto si valida
|
||||
sul trasporto (P2). Tre correzioni: (1) `revisione.interroga` proprio, prompt su **stdin**, ogni eccezione
|
||||
restituita come errore; (2) `scrivi` incapsula chiamata e invio: un'eccezione diventa un rapporto «NON
|
||||
eseguita» + 🚨; (3) `_cmd` (crontab, git) dichiara qualunque eccezione invece di propagarla. Test con
|
||||
controllo positivo su `OSError(7)`. Terzo difetto, trovato dal test: `analista.pulisci` **toglie le righe
|
||||
che iniziano con `#`**, cioe' i cinque titoli che la guardia richiede — usarlo avrebbe reso ogni revisione
|
||||
«rifiutata: titoli mancanti». Sostituito con `strip()`.
|
||||
|
||||
Secondo lancio: **rapporto scritto, stato `sospetta`**, 1.435 parole, `docs/revisioni/2026-09-09.md`.
|
||||
La guardia ha segnato 11 numeri «non nel materiale»: sono cifre in **formato italiano** («4.477,46»,
|
||||
«1.341») contro il materiale in formato USA («$4,477.46») — la normalizzazione di `analista` conosce una
|
||||
convenzione sola. E' un limite dichiarato, non un errore del revisore: si aggiusta quando si vede quanto
|
||||
spesso scatta. Contenuto: **nessun URGENTE**; tre verifiche in cima (una possibile attribuzione a reward di
|
||||
400 USDE che erano un acquisto in `usde_watch` del 07/09 — `n_trades` fermo a 50 contro 54 ordini nel
|
||||
registro; il cuscino USDC del debito §5.17 senza definizione balance/equity; il versamento del 04/09 non
|
||||
dichiarato in `movimenti_dichiarati.jsonl`); una contraddizione in CLAUDE.md §1 sull'haircut (M28); la data
|
||||
di arming che differisce fra §0, config e TWR; quattro sorveglianti senza timestamp o fuori da
|
||||
`monitor_health`; e, come richiesto dalla regola 5, **zero idee di strategia**. «Cosa mi mancava»: stati
|
||||
con ora di `edge/fee/venue/scale_watch`, l'output di `usde_watch.rendimento()`, i paper a oggi,
|
||||
`cron_daily.sh`, il registro fill della settimana — l'elenco da cui il materiale della prossima settimana
|
||||
migliora. Le segnalazioni sono dell'operatore e della prossima sessione: **nessuna e' stata applicata qui**
|
||||
(sarebbe l'agente che esegue le proposte del revisore nello stesso giro, cioe' il ciclo che si e' scartato).
|
||||
Costo: una chiamata, ~4 minuti.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Giornale di bordo — 2026-09-09
|
||||
|
||||
*Scritto 2026-09-10T00:38:03+00:00. Numeri misurati; nessuna interpretazione automatica.*
|
||||
|
||||
## Mercato
|
||||
|
||||
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| **BTC** | $78,283.50 | -0.24% | +1.26% | +22.45% | 46.8% | ↑ ↑ ↑ (3/3 su) | 40.2 (42° pctl 1a) |
|
||||
| **ETH** | $2,467.95 | -0.70% | +3.19% | +31.89% | 70.1% | ↑ ↑ ↑ (3/3 su) | 53.7 (32° pctl 1a) |
|
||||
|
||||
## Libro
|
||||
|
||||
Equity **$4,472.10** · nozionale lordo $1,157 · leva lorda 0.26x · barra dati 2026-09-09 · 24 giri
|
||||
|
||||
| | TP01 | SKH01 | target | posizione | azione |
|
||||
|---|---|---|---|---|---|
|
||||
| **BTC** | +0.418 | flat | $+701 | $+704 | HOLD (a target) |
|
||||
| **ETH** | +0.270 | flat | $+453 | $+453 | HOLD (a target) |
|
||||
|
||||
## P&L
|
||||
|
||||
- giorno: **$-5.36** di equity (24 letture)
|
||||
- realizzato: +0.00 netto su 0 round-trip chiusi · 2 fill · fee 0.0047
|
||||
- cumulato dall'arming: **$+3,874.04** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+39.61** · TWR **+7.28%**
|
||||
|
||||
## Salute
|
||||
|
||||
- giri di `book_execute`: 24/24
|
||||
- eta' feed SKH all'ultimo giro: 0 min
|
||||
|
||||
## Lettura
|
||||
|
||||
*Generata da regole dichiarate in `src/live/journal.py` — combina i numeri qui sopra e nient'altro. L'id fra parentesi quadre dice quale regola ha parlato. Non e' un giudizio: quello sta nella Nota.*
|
||||
|
||||
- `[stato]` A mercato su BTC $+704 (TP01 +0.418, SKH01 flat) e ETH $+453 (TP01 +0.270, SKH01 flat).
|
||||
- `[leva]` Leva lorda **0.26x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.74x.
|
||||
- `[pnl]` Equity $-5.36 con 2 fill e 0 round-trip chiusi (realizzato $+0.00 netto).
|
||||
- `[drawdown]` 📌 Equity **-0.68%** sotto il picco ($4,502.93). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
|
||||
- `[vol]` BTC: implicita 40.2 contro realizzata 30g 46.8 (sotto); DVOL al 42° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||
- `[vol]` ETH: implicita 53.7 contro realizzata 30g 70.1 (sotto); DVOL al 32° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
|
||||
- `[evidenza]` 📌 Campione a oggi: **78 giorni, 42 round-trip**. A questa taglia il P&L non distingue l'edge dalla fortuna — il criterio di morte del libro (`edge_watch`) lavora su una finestra di 36 mesi.
|
||||
|
||||
## Analisi (agente)
|
||||
|
||||
*Scritta da `claude-opus-5` il 2026-09-10T00:39:20+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
|
||||
|
||||
Il fatto nuovo non e' il segno del giorno ma la sua composizione: $-5.36 di equity con 2 fill, zero round-trip chiusi e realizzato $+0.00. La perdita e' tutta marcatura su posizioni aperte, e le posizioni sono a target su entrambe le gambe ($+704 contro $+701, $+453 contro $+453): il libro non ha deciso niente, ha ri-tagliato. Nella serie qui sopra l'unico altro giorno senza round-trip chiusi e' il 01/09; e' il terzo giorno di equity in discesa dopo il picco del 06/09 a $4,502.93, con BTC -0.24% e ETH -0.70%.
|
||||
|
||||
L'esposizione la porta interamente TP01 (+0.418 su BTC, +0.270 su ETH) mentre SKH01 e' flat su entrambi, e questo con TSMOM 3/3 su e 30 giorni a +22.45% e +31.89%: il trend c'e', il breakout no. Esecuzione pulita, 24/24 giri e feed SKH a 0 min.
|
||||
|
||||
L'implicita sotto la realizzata (40.2 contro 46.8; 53.7 contro 70.1) si legge in due modi ugualmente compatibili — vol a sconto, oppure realizzata gonfiata dalla corsa dei 30 giorni. Quale dei due, non e' misurabile qui.
|
||||
|
||||
Da guardare senza urgenza: il cumulato $+3,874.04 e' quasi tutto versamento ($+3,834.43), il trading vale $+39.61 su 42 round-trip in 78 giorni. A quella taglia il TWR +7.28% non e' ancora una misura di edge.
|
||||
|
||||
## Nota
|
||||
|
||||
*(vuota — campo dell'operatore)*
|
||||
@@ -701,7 +701,7 @@ chi lo riapre deve battere il motivo, non ripetere l'esperimento.
|
||||
**0,93** (alto) e **supera 1 in backwardation** mentre `f_skew` resta piatto -> **la parte maggiore
|
||||
del difetto si annulla proprio dove il gate fa tradare il sleeve**, quindi f=0,73 e' plausibilmente
|
||||
**conservativo li'**. Non misurato (0/22 ingressi passano il gate) e in un crash i due pezzi vanno
|
||||
in **direzioni contrarie**. **`VRP_CFG["f"]` NON cambiato.** Replica indipendente del f: **0,714**.
|
||||
in **direzioni contrarie**. **`VRP_CFG["f"]` NON cambiato.** Replica indipendente del f: **0,714** (0,712 dal 09/09 sera, `cblib` causale; il 0,73 del 30/07 stampa 0,71).
|
||||
📌 **(7) CHIUSURE DI FAMIGLIA.** **Il filone funding si chiude sul QUARTO lato** (affollamento):
|
||||
su **53.430 ore / 3 anni** non predice ne' direzione ne' volatilita'. E i futures datati **non sono
|
||||
un quinto lato**: il loro basis **E'** il funding del perp (+7,33% implicito contro **+6,48%**
|
||||
@@ -1532,7 +1532,7 @@ una maschera a caso della stessa frequenza**.
|
||||
−3,38%: crollo con TP01 a 0,505 (lo squeeze di §33 e' sulla lente hourly). 37,5/62,5 batte 12/12 episodi
|
||||
ma maxDD 12,1% vs 9,4%; gate fallisce in-sample (cache `skh_sigtab` ferma al 25/07). **(2) Crollo
|
||||
catturato**: l'archivio bite copre con bid/ask il 1-5/06/2026 (BTC −25%, ETH −30%), scadenza 19/06, inverse.
|
||||
ETH pre-crollo (4 strutture): f_net **0,74 = 0,714 del rally**; f a scadenza 1,04 ma **tautologico** (ambo le
|
||||
ETH pre-crollo (4 strutture): f_net **0,76 [0,71-0,86] = 0,712 del rally** (rimisurato la sera con `cblib` causale; era 0,74 = 0,714); f a scadenza 1,04 ma **tautologico** (ambo le
|
||||
gambe ITM ⇒ solo f_net); peggior MTM **1,08** dopo il taglio alla larghezza — la prima stesura diceva 2,58 con
|
||||
"IV corta esplosa a 108 per lo skew": **artefatto** (quote 158% di spread a T→0; la IV 108 e' del 18/06, a T→0);
|
||||
put δ−0,10 costa **1,92×** il modello (punto nuovo, sopra l'1,27 di §46) e paga come il modello (0,94-1,04);
|
||||
@@ -1544,7 +1544,8 @@ una maschera a caso della stessa frequenza**.
|
||||
Coinbase delisto' XRP 2021-01→2023-07, riferimento Bitstamp prima); 5m flat 27-61%. Dato in
|
||||
`data/raw/alt_xrp_*`. **(4) Venue**: 32 perp USDC, liquidi ≥$10M/g solo XRP/ETH/BTC/SOL; XS01 13/19 (APT
|
||||
inactive); `BTCDVOL_USDC` future esiste ma vol 0 / spread 12%; PAXG perp non misurato (9 mesi, $0,18M/g).
|
||||
**Debito nuovo (§5.18)**: `cblib.spot_series` + `asof` guarda un'ora avanti (feed 1h etichettato
|
||||
all'apertura). **Errori dell'autore smontati dal revisore `fable`**: il 2,6× (artefatto) e "8/12 GUADAGNA"
|
||||
**Debito §5.18** (`cblib.spot_series` + `asof` un'ora avanti) **riparato la sera stessa**, insieme al DVOL
|
||||
(fino a 24h avanti): §11 0,714 → 0,712, verdetti invariati; `vrp_f_watch` f canonico 0,732 → 0,706 e la
|
||||
differenza col candidato +0,097 [+0,042, +0,140] ora esclude lo zero (criterio (a) 28/40 ancora aperto). **Errori dell'autore smontati dal revisore `fable`**: il 2,6× (artefatto) e "8/12 GUADAGNA"
|
||||
(l'ordine delle classi era il verdetto: immune → POSITIVO da' 4+6). Nessun cambio a libro/pesi/config.
|
||||
|
||||
|
||||
@@ -1011,3 +1011,39 @@ impedire.** Danno reale quel giorno: nessuno — alle 09:47 l'equity era leggibi
|
||||
che si accende se la fixture viene rimossa.
|
||||
- ⚠️ **Resta il principio più largo:** oggi è deviato **solo** il watermark. `data/live/trades.db` e
|
||||
`data/live/book_executions.jsonl` sono esposti allo stesso errore (§5.12).
|
||||
|
||||
### Revisione settimanale in sola lettura (2026-09-09)
|
||||
|
||||
L'operatore ha chiesto «un processo di auto-revisione dei dati e della configurazione, che si autoregola,
|
||||
si autoaggiorna, crea sistemi nuovi valutando mercato e dati macro, e mi informa su Telegram». La risposta,
|
||||
misurata contro la memoria, e' stata in quattro pezzi: **la rilettura periodica manca e vale** (i nove
|
||||
sorveglianti guardano ognuno la sua grandezza; nessuno rilegge il sistema intero, e la revisione con un
|
||||
secondo modello ha trovato in una sessione due conclusioni false e un look-ahead); **l'autoregolazione dei
|
||||
parametri no** (e' cio' che `scale_watch`/`venue_watch` trattano come anomalia; A8 della chiave di scala:
|
||||
una riga di config, una di giornale, decisione dell'operatore); **la generazione automatica di strategie
|
||||
solo con un freno** (ogni candidato e' un trial, M3, e la ricerca non e' il vincolo binding dal 26/07: il
|
||||
miglior lead vale +0,046 €/giorno contro +4,07%/anno di drift per €100/mese in piu'); **il macro da
|
||||
internet no** (gate macro, DVOL direzionale e skew de-risk sono gia' HEDGE/ridondanti; un dato dal web
|
||||
non e' certificato — la APR USDC «pubblicata» e mai incassata). L'operatore ha scelto la prima («fai»).
|
||||
|
||||
Cosa gira: `scripts/cron_review.sh` (lunedi' 06:15 UTC, fuori dal :00 e dal martedi' 09:00, dopo il
|
||||
`cron_daily` delle 00:30) → `scripts/live/revisione.py --quiet` → `src/live/revisione.scrivi`. Materiale
|
||||
(~195k caratteri, sola lettura, ordine: regole e stato dichiarato, cio' che gira, cio' che e' successo):
|
||||
CLAUDE.md intero, `config/live.json`, `crontab -l`, `git log` 14 g + `git status`, `monitor_health`,
|
||||
stato dei sorveglianti (scale, vrp_f, usde, balance, venue_news, movimenti dichiarati, watermark — messi
|
||||
PRIMA delle voci grandi, cosi' il tetto totale non li taglia mai: alla prima stesura `usde_convert` era
|
||||
finito a 2 caratteri), 7 pagine di giornale, i diari della settimana. Prompt con dieci regole: sola
|
||||
lettura, ogni segnalazione cita la fonte, solo numeri del materiale (guardia `numeri_non_supportati` di
|
||||
`analista`, soglia 12), le decisioni vincolanti di §3 non si ripropongono, un'idea nuova si propone solo
|
||||
citando la memoria che ha ucciso la simile, i gate si elencano con data e criterio senza anticipare il
|
||||
verdetto, misurato ≠ dedotto, niente previsioni, URGENTE per cio' che serve all'operatore subito, cinque
|
||||
titoli fissi. Rapporto in `docs/revisioni/<data>.md` firmato (modello, data, «lettore fallibile P13»,
|
||||
tabella del materiale letto con caratteri e troncature); Telegram = la sola Sintesi, con taglio dichiarato.
|
||||
Modello `claude-fable-5-1` via `analista.interroga` (CLI `claude -p`, `--allowed-tools ""`, timeout 900 s):
|
||||
**non il modello che scrive il codice**, ed e' un test (`test_il_revisore_non_e_il_modello_che_scrive_il_codice`).
|
||||
Un modello muto o una risposta senza i titoli produce un rapporto «NON eseguita» col motivo e un 🚨: il
|
||||
silenzio non e' una revisione (P5). Guardie: `valida` di `cli` come prima istruzione (`test_cli_flag` lo
|
||||
deriva dalla cartella), `--secco` non chiama e non scrive (test sull'albero dei file), un giro scrive **UN
|
||||
solo file** (test), la cadenza e' dichiarata nel `.sh` e letta dalla crontab installata (`test_revisione`,
|
||||
stesso schema di `test_book_cadenza`). Costo: una chiamata a settimana. Cio' che il sorvegliante non puo'
|
||||
fare: eseguire. E' voluto.
|
||||
|
||||
@@ -164,8 +164,13 @@
|
||||
con quell'etichetta restituisce la chiusura di **ts+1h**: un look-ahead di un'ora senza eccezioni e
|
||||
senza NaN (D6, quarta occorrenza). Colpisce `cblib.spot_series` — cioe' il f 0,714 di §11 e ogni numero
|
||||
di `cblib` con lo spot, compreso `ST = S.asof(exp)` che e' il prezzo di un'ora DOPO la scadenza.
|
||||
`r0909_crash_catturato.spot_causale` sposta l'indice di +1h (chiusura = tempo in cui e' nota); `cblib`
|
||||
**non e' riparato** (cambierebbe numeri pubblicati: va fatto con la rimisura di §11). Debito CLAUDE.md §5.18.
|
||||
✅ **RIPARATO la sera stessa in `cblib`** (`causale(s, cadenza)`: spot +1h, e **anche il DVOL
|
||||
giornaliero, +1 giorno** — la riga del giorno D e' la chiusura delle 23:00 di D, verificato contro la
|
||||
risoluzione 1h dell'API pubblica: seconda serie con lo stesso difetto, quinta occorrenza di D6). §11
|
||||
riprodotto al millesimo su worktree HEAD e rimisurato: 0,714 → **0,712** (le correzioni hanno segno
|
||||
opposto; DVOL orario di controllo 0,717). Il DVOL causale da feed giornaliero e' **stantio fino a 23 ore**
|
||||
(0,2 pt mediana, 3 pt max; nel crollo ±0,05 di f): il feed DVOL orario e' il passo aperto. Diario
|
||||
`2026-09-09b-debito-18-cblib-causale.md`.
|
||||
- **XRP** (`XRP/USDC:USDC`, dal 2022-03-16) ricostruito il 2026-09-09 con `rebuild_history --asset XRP` e
|
||||
spostato in **`data/raw/alt_xrp_*.parquet`** (namespace di ricerca come SOL: fuori dal feed attivo, non
|
||||
rinfrescato dal cron, `load_data("XRP")` fallisce, presidiato da `test_sol_leg` via `DERIBIT_INSTR`).
|
||||
|
||||
@@ -197,6 +197,18 @@ lati contro il **93%** di BTC_USDC, e il suo miglior bid sta a **un tick**.
|
||||
DVOL alla mediana del 7°/11° percentile storico, sottostante +25%/+44%.
|
||||
- ✅ **Replica indipendente del f del 30/07 su campione piu' lungo: f = 0,714 pooled** (IC95
|
||||
[0,690, 0,779], **0/19 osservazioni >= 1,0**). Attraversare lo spread costa **~10% del credito**.
|
||||
✅ **RIMISURATO il 2026-09-09 sera** (debito §5.18: `cblib` dava lo spot di un'ora DOPO e il DVOL di
|
||||
fino a 24 ore DOPO l'ingresso): allo stesso taglio (22/08, n=19) **f = 0,712 [0,664, 0,732]** — le due
|
||||
correzioni hanno segno opposto (solo spot 0,721, solo DVOL 0,693) e un DVOL orario di controllo da'
|
||||
0,717; a oggi (n=23) 0,712 [0,677, 0,741]. **Il verdetto non cambia.** Cambiano i numeri di
|
||||
contorno: Sharpe canonico BTC 32,58 → 29,61 con mediana onesta **2,12** (era 1,45) e banda
|
||||
[−0,19, 29,61]; ETH 3,90 → 7,64 (mediana onesta 3,35); media BTC fra le ancore −0,66% → +11,44%
|
||||
(cambia ancora segno); forfait fee 1,51× (BTC) / 1,87× (ETH); il regolamento e' il prezzo delle
|
||||
08:00 e non delle 09:00 (P&L +3,3%); sottostante nel campione +21% / +40% (era +25% / +44%: il vecchio
|
||||
regolamento del 21/08 era il prezzo delle 09:00, **+3,9%** sopra quello delle 08:00 — quanto pesa un'ora su
|
||||
un numero a due estremi). Lo stesso `cblib` rigira anche `r0730`: il f 0,73 [0,706-0,805] del 30/07 stampa
|
||||
ora **0,71 [0,680-0,756]** (n=27, 0/27 ≥ 1).
|
||||
Diario `2026-09-09b-debito-18-cblib-causale.md`.
|
||||
- ⚠️ **Correzione a un numero pubblicato:** il forfait fee del sleeve sovrastima il listino vero di
|
||||
**1,44x (BTC) / 1,80x (ETH)**, non di "~2x". Il segno (conservativo) regge, la taglia no.
|
||||
- ⚠️ **Rischio sul MINIMO, non sulle chiusure:** peggior mark infra-settimana mediana −6,4% del
|
||||
@@ -374,6 +386,13 @@ nuovo: **va decisa PRIMA del 23/10, non quel giorno.**
|
||||
(il collettore li accumula da solo), revisione comunque **2027-02-22**.
|
||||
- ✅ Confound di modello **ESCLUSO e misurato**: `f_markfit` = **0,992** (la mark IV di Deribit
|
||||
riprezza i propri mid entro l'1%) -> RR/BF sono un fatto di **prezzo**, non l'output del fit.
|
||||
📌 **Rigirato il 2026-09-09 sera con spot e DVOL causali** (debito §5.18): la scomposizione sul panel si
|
||||
muove di **≤0,005** per fattore (term 0,880 · skew 0,917 · spread 0,910 · fit 0,995 · f_tot 0,729) e Q1/Q2
|
||||
non si toccano (serie proprie); agli **ingressi** il controllo `f_markfit` passa da 1,045 a **0,991** e
|
||||
le osservazioni che lo superano da 22/28 a **25/28** — l'ora di spot in piu' stava nel controllo, non
|
||||
nell'effetto. Agli ingressi (n=28) si muove di piu': f_term 0,910 → 0,875, f_tot 0,732 → 0,706, e le
|
||||
correlazioni `corr(f_tot_mid, rr7)` +0,21 → −0,06 e `atm7` +0,02 → +0,34 **cambiano segno/ordine** — sono
|
||||
22-25 punti, dichiarate (P12), non usate.
|
||||
- ✅ **Controllo positivo cablato nello script:** con l'ancora **assunta** a :30 invece del `ts_max`
|
||||
osservato compare un **falso lead** a +15m (0,102 contro 0,031) — *"e' l'errore che avrei
|
||||
pubblicato senza quel controllo"*.
|
||||
@@ -3997,7 +4016,7 @@ nuovo: quello che guadagna nei crolli e' gia' in produzione, e la leva che lo au
|
||||
|
||||
`scripts/research/r0909_crash_catturato.py`. **Nessun ordine.** L'archivio bite copre con bid/ask il crollo
|
||||
1-5/06/2026 (BTC −25%, ETH −30%; scadenza 19/06, 14-21 DTE, famiglia INVERSE); il campione di §11 partiva
|
||||
dal fondo. ETH, ingressi pre-crollo (286 snapshot = **4 strutture**): f_net **0,74 = 0,714 del rally**;
|
||||
dal fondo. ETH, ingressi pre-crollo (286 snapshot = **4 strutture**): f_net **0,76 [0,71-0,86] = 0,712 del rally** (era 0,74 = 0,714 prima della riparazione di §5.18, la sera stessa; put 1,98× e picco 0,99 idem);
|
||||
f a scadenza 1,04 ma **tautologico** nell'83% (ambo le gambe ITM ⇒ solo f_net); peggior MTM **1,08**
|
||||
dopo il taglio alla larghezza; put δ−0,10 costa **1,92×** il modello (punto nuovo sopra l'1,27 di §46)
|
||||
e paga come il modello (0,94-1,04); ingressi durante il crollo bimodali (perde il 46%). BTC pre-crollo
|
||||
@@ -4006,9 +4025,11 @@ IV-rank di VRP01 CHIUSO in tutte le ore. ⇒ la condizione di §3 e' soddisfatta
|
||||
sostanza: non si riapre**. Servono un crollo con IV-rank > 0,30 e la famiglia USDC-lineare.
|
||||
🚨 **Due conclusioni dell'autore smontate in revisione:** «MTM 2,58× per lo skew (IV 108)» era un
|
||||
artefatto (quote con 158% di spread a T→0 oltre la larghezza; la IV 108 e' del 18/06 a T→0); «f a
|
||||
scadenza 1,04 = il modello prezza bene» era una tautologia. **Debito nuovo (CLAUDE.md §5.18):**
|
||||
`cblib.spot_series` + `asof` guarda un'ora avanti (feed 1h etichettato all'apertura); qui corretto con
|
||||
`spot_causale`, in `cblib` no (toccherebbe il 0,714 di §11).
|
||||
scadenza 1,04 = il modello prezza bene» era una tautologia. **Debito §5.18 RIPARATO la sera stessa** in `cblib`
|
||||
(spot +1h, e anche il DVOL giornaliero, +1 giorno): ETH pre-crollo f_net **0,76 [0,71-0,86]** (era 0,74),
|
||||
BTC durante **0,89 [0,74-1,05]** (era 0,79; con DVOL orario 0,83 — nel crollo il DVOL giornaliero stantio
|
||||
vale −0,04/+0,06 di f), put δ−0,10 1,98× (era 1,92), DVOL pre 36,4 / 49,6 (il 37,3 di BTC era la chiusura del
|
||||
PRIMO giorno di crollo: contaminato). Picco e percentili invariati. Verdetto invariato.
|
||||
**VERDETTO: `CATTURATO, MA A VOL BASSA E FUORI DAL GATE: f_net NON CAMBIA COL REGIME`** — nessuna riapertura.
|
||||
|
||||
## 76 — XRP-TERZA-GAMBA (l'unica altra moneta liquida di Deribit, coi meccanismi congelati)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# Revisione settimanale — 2026-09-09
|
||||
|
||||
*Scritta il 2026-09-09T21:59:07Z dal modello `claude-fable-5-1`, revisore diverso dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: **sospetta** — 11 numeri non presenti nel materiale: 1.011, 1.341, 1.38279, 1.40085, 2.034, 2.43408, 4.47099, 4.47746, 400, 40008, 6879.*
|
||||
|
||||
## Sintesi
|
||||
|
||||
Revisione del 2026-09-09 21:59Z (primo giro, manuale: il cron e' del lunedi'). **Nessun URGENTE.** Nella settimana 02-08/09 il libro ha fatto 24/24 giri ogni giorno con feed SKH a 0 min, leva lorda 0,26x, equity $4.477,46 e TWR +7,41% (giornale 08/09). Tre cose da verificare, in ordine:
|
||||
|
||||
1. **`usde_watch` del 07/09 ha stimato un reward di 400,08 USDE**, dove i giorni normali (08-09/09) danno 0,38. E' la differenza fra i 2.434,08 USDE entrati e i 2.034,0 contati come trade, con `n_trades` fermo a **50**. Il registro `usde_convert.jsonl` mostra 8 + 30 + 16 = 54 ordini: ne mancano 4 da 100 = 400. Un lettore che vede al massimo 50 trade attribuisce al reward cio' che era un acquisto (D2, P2). Se `rendimento()` usa quella finestra, il tasso che `quota_da_decidere()` stampa ogni giorno e' inquinato.
|
||||
2. **Il cuscino USDC (debito §5.17) resta senza sorvegliante, e la sua misura non e' definita**: a `balance_watch` 21:42Z l'USDC ha balance 1.400,85 ma equity 1.382,79; con l'equity totale 4.470,99 (watermark) il cuscino al 30% vale ~$1.341, quindi lo slack e' **+$59 sul balance o +$41 sull'equity USDC**. Quale dei due conta va deciso prima di scrivere il sorvegliante.
|
||||
3. **Operatore**: il versamento di $2.410,14 del 04/09 e' solo *rilevato* (salto 13:47→14:47); `movimenti_dichiarati.jsonl` contiene il solo €25 del 25/08 e la Nota di giornale e' vuota 7/7 giorni. CLAUDE.md §2 chiede la dichiarazione il giorno stesso, e §1 («soldi fermi») dice ancora «in attesa di importo e data» per il piano €5.000: non si capisce se il 04/09 ne e' il primo pezzo.
|
||||
|
||||
## Cosa non torna
|
||||
|
||||
- **«2.434 USDE in 39 ordini»** (diario `2026-09-06-usde-quota-topup` §3, CLAUDE.md §1) non si riproduce dal registro: 8 ordini nel primo record, 30 eventi nella prima sonda, 16 nella seconda. Il totale USDE torna, il conteggio no (N11).
|
||||
- **Haircut, due affermazioni opposte in CLAUDE.md §1** (M28): una dice che il 5% e' confermato dal passaggio a X:SM (`available_funds` $2.011,09 contro $2.010,90 attesi *con* haircut 5%); poche righe sotto «oggi comunque non si applica», frase del mattino del 31/08 sotto S:SM. `config/live.json` `_nota_usde` dice ancora «il modello ATTIVO e' Segregated» e giustifica `depeg_crit 0.95` come «meta' del buffer di haircut (10%)»: col 5% il criterio dichiarato (P6) non descrive piu' la soglia, e la nota e' una diagnosi cablata (P4).
|
||||
- **GATE SCALA-01: data e condizione A2 non concordano** (CLAUDE.md §4). «Non prima del 2026-10-01» sono 30 giorni dal 01/09; A2 chiede k0 «≥180 g col criterio passato ogni giorno», e il criterio lo misura solo `scale_watch`, attivo dal 01/09 (`scale_history.jsonl`). 180 giorni dal 01/09 cadono nel 2027; dal 20/06 a fine 2026. Da quando si conta va chiarito prima del 01/10, non quel giorno.
|
||||
- **Sorveglianti di cui il materiale non prova il funzionamento** (P5): `venue_news.jsonl` ha 5 voci tutte `seed: true` viste il 2026-08-31T07:40Z e nulla dopo; `scale_watch_state.json` non ha timestamp (streak 0 puo' essere «non ho girato»); `edge_watch` e' citato a «stato 27/07» (CLAUDE.md §4); `fee_watch` non compare. `monitor_health` copre 8 monitor, nessuno di questi quattro.
|
||||
- **Data di arming**: CLAUDE.md §0 «LIVE dal 2026-06-20», `config/live.json` «ARMATO 2026-06-23», primo segmento TWR dal 2026-06-23 (diario `2026-09-02-debito-14`). Una delle tre non e' la stessa data.
|
||||
- **Richieste dell'analista senza risposta**: i 4 fill del 03/09 («dovrebbero essere ri-taglie»), la conferma del versamento 04/09, il SELL $−7 del 06/09, il round-trip a $+0,00 con fee 0,0055 del 08/09. Nessuna Nota. Non e' un guasto: e' un canale che non chiude (N9).
|
||||
- **CLAUDE.md §13 dice 1.011 test**; il diario `2026-09-09c` ne aggiunge 16 (`test_revisione.py`). E `scripts/cron_review.sh`, `scripts/live/revisione.py`, `src/live/revisione.py` sono `??` in `git status` mentre la crontab li chiama lunedi' 06:15 (P16 li fa girare, P11 non li salva).
|
||||
|
||||
## Cosa e' maturo da decidere
|
||||
|
||||
| gate | data | criterio scritto | stato nel materiale |
|
||||
|---|---|---|---|
|
||||
| STATARB | 2026-09-27 (fra 18 g) | soglie 24/07 + diagnostica sempre-short | monitor OK; Sharpe −1,61 al 26/08, nessuna lettura piu' recente. Si legge il 27/09 |
|
||||
| SCALA-01 | non prima del 2026-10-01 (22 g) | 9 condizioni tutte necessarie | A8/A9 fatti; A2 non maturo e ambiguo (sopra); A3, A6, A7 non risultano fatti |
|
||||
| USDE-01 | chiuso 28/08 | — | quota 68,79% (09/09) sotto `quota_max_frac` 0,85: dentro la decisione dell'operatore del 30/08 e 06/09 |
|
||||
| PREVDAY-01 | **senza data** | 10 condizioni, 8/10 | N10: un gate senza data non si chiude |
|
||||
|
||||
XSR01 (23/10) e DVOLSPREAD (24/10) cadono oltre 30 giorni. `edge_watch` e' continuo ma l'ultimo stato e' del 27/07: non determinabile se sia ancora 8/8.
|
||||
|
||||
Decisioni dell'operatore, oggi: (a) dichiarare il 04/09; (b) dire se il piano €5.000 e' iniziato; (c) definire il cuscino (balance o equity USDC). Nessuna decisione di §3 ha la colonna «cosa la riapre» soddisfatta: `weights_tilt_null` fallisce ancora (26/07, in-sample); il crollo catturato e' a vol bassa, sotto il gate e inverse (diario 09/09 §2).
|
||||
|
||||
## Proposte (nessuna azione eseguita)
|
||||
|
||||
1. **Agente**: verificare in `usde_watch` il limite di lettura dei trade e come `rendimento()` tratta la finestra del 07/09; rigenerare la stima con i 54 ordini (P2).
|
||||
2. **Agente**: riallineare `_nota_usde` (modello attivo X:SM, criterio di `depeg_crit` al 5%, `venue_cap_frac` null) e sciogliere la contraddizione di §1 datando le due frasi (M28).
|
||||
3. **Agente + operatore**: il sorvegliante del cuscino e' gia' descritto in §5.17 (`USDC − cuscino_richiesto` derivato da config, P1); manca solo la scelta balance/equity.
|
||||
4. **Agente**: uno stato senza timestamp non distingue «zero» da «non ho girato» (P5): `scale_watch_state.json` e `venue_news` meritano l'ora dell'ultimo giro, e i quattro sorveglianti fuori da `monitor_health` una riga li'.
|
||||
5. **Agente**: committare i file della revisione, aggiornare il conteggio test.
|
||||
6. **Operatore**: dare una data a PREVDAY-01 o chiuderlo (N10).
|
||||
7. Nessuna idea di strategia: la settimana ha gia' misurato crolli, opzioni e XRP contro `20-ondate` (diario 09/09 §0) e non ho nulla che batta i motivi elencati li'.
|
||||
|
||||
## Cosa ho letto e cosa mi mancava
|
||||
|
||||
Letto: CLAUDE.md intero; `config/live.json`; crontab; git log dal 26/08 e status; `monitor_health`; stati di scale_watch, scale_history, vrp_f_watch, usde_watch, usde_convert, balance_watch, venue_news, movimenti_dichiarati, watermark; giornali 02-08/09; diari 02/09 (tre), 06/09, 09/09 (tre).
|
||||
|
||||
Mancava: (1) stato con timestamp di `edge_watch`, `fee_watch`, `venue_watch`, `scale_watch`; (2) l'output di `usde_watch.rendimento()` e `quota_da_decidere()`; (3) la lettura corrente di `paper_statarb` (Sharpe a oggi, non al 26/08), dvolspread, xsr; (4) il contenuto di `cron_daily.sh`, cioe' quali sorveglianti chiama; (5) `venue_news` dopo il 31/08; (6) il diff di CLAUDE.md e memoria 40 non committati; (7) il registro fill della settimana (per i 4 fill del 03/09 e il +0,00 del 08/09); (8) un registro depositi del gateway, che «non espone get_deposits».
|
||||
|
||||
---
|
||||
## Firma e materiale
|
||||
|
||||
- revisore: `claude-fable-5-1` · cadenza settimanale · giornale 7 g · diari 7 g · commit 14 g
|
||||
- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file scritto e' questo
|
||||
|
||||
| voce | caratteri | troncata |
|
||||
|---|---|---|
|
||||
| CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole) | 80,310 | |
|
||||
| config/live.json (unica autorita' dei guardrail live) | 7,184 | |
|
||||
| crontab installata (crontab -l) | 1,312 | |
|
||||
| git log dal 2026-08-26 + stato del working tree | 4,543 | |
|
||||
| monitor_health (i forward-monitor stanno registrando?) | 522 | |
|
||||
| stato scale_watch_state.json | 149 | |
|
||||
| stato scale_history.jsonl (ultime 3) | 259 | |
|
||||
| stato vrp_f_watch/state.json | 453 | |
|
||||
| stato usde_watch.jsonl (ultime 3) | 1,339 | |
|
||||
| stato usde_convert.jsonl (ultime 3) | 9,979 | |
|
||||
| stato balance_watch.jsonl (ultime 2) | 578 | |
|
||||
| stato venue_news.jsonl (ultime 5) | 1,564 | |
|
||||
| stato movimenti_dichiarati.jsonl | 405 | |
|
||||
| stato equity_seen.json (watermark) | 73 | |
|
||||
| giornale 2026-09-02 | 3,824 | |
|
||||
| giornale 2026-09-03 | 3,745 | |
|
||||
| giornale 2026-09-04 | 3,999 | |
|
||||
| giornale 2026-09-05 | 3,645 | |
|
||||
| giornale 2026-09-06 | 3,630 | |
|
||||
| giornale 2026-09-07 | 3,846 | |
|
||||
| giornale 2026-09-08 | 3,765 | |
|
||||
| diario 2026-09-02-debito-14-report-twr.md | 6,952 | |
|
||||
| diario 2026-09-02b-debito-7-cadenza-docstring.md | 6,061 | |
|
||||
| diario 2026-09-02c-flag-e-pulizia.md | 5,874 | |
|
||||
| diario 2026-09-06-usde-quota-topup.md | 6,571 | |
|
||||
| diario 2026-09-09-crolli-opzioni-monete.md | 17,894 | |
|
||||
| diario 2026-09-09b-debito-18-cblib-causale.md | 14,150 | |
|
||||
| diario 2026-09-09c-revisione-settimanale.md | 5,514 | |
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Revisione SETTIMANALE in sola lettura (dal 2026-09-09): un revisore diverso dall'autore
|
||||
# (claude-fable-5-1) rilegge CLAUDE.md, config, crontab, commit, monitor_health, giornale e diari,
|
||||
# e scrive docs/revisioni/<data>.md + una sintesi Telegram. Propone, non esegue: l'unico file
|
||||
# scritto e' il rapporto. Cadenza: lunedi' 06:15 UTC — fuori dal minuto :00 (rate-limit) e dallo
|
||||
# slot di release Deribit (martedi' 09:00), dopo il cron_daily delle 00:30 (giornale di domenica chiuso).
|
||||
export PATH="/home/adriano/.local/bin:$PATH"
|
||||
cd /opt/docker/PythagorasGoal || exit 1
|
||||
mkdir -p logs
|
||||
{
|
||||
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_review ====="
|
||||
uv run python scripts/live/revisione.py --quiet || true
|
||||
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||
} >> logs/cron_review.log 2>&1
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Revisione settimanale in SOLA LETTURA: un modello DIVERSO dall'autore rilegge il sistema.
|
||||
|
||||
uv run python scripts/live/revisione.py # chiama il revisore, scrive docs/revisioni/<data>.md, Telegram
|
||||
uv run python scripts/live/revisione.py --secco # solo la taglia del materiale: niente modello, niente file
|
||||
uv run python scripts/live/revisione.py --quiet # per il cron (scripts/cron_review.sh, lunedi' 06:15 UTC)
|
||||
|
||||
Cosa scrive: SOLO il rapporto. Cosa non tocca: config, libro, cron, codice, dati. Le proposte
|
||||
restano proposte; decide l'operatore. Dettagli e ragioni in `src/live/revisione.py`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
USO = """uso: revisione.py [--giorno AAAA-MM-GG] [--modello NOME] [--secco] [--no-telegram] [--quiet]
|
||||
--giorno G data della revisione (default: adesso, UTC)
|
||||
--modello M revisore (default: src.live.revisione.MODELLO = claude-fable-5-1 — NON il modello che scrive il codice)
|
||||
--secco stampa la taglia del materiale e si ferma: niente modello, niente file, niente Telegram
|
||||
--no-telegram scrive il rapporto ma non manda la sintesi
|
||||
--quiet stampa solo l'esito
|
||||
⚠️ SPENDE una chiamata al modello e MANDA un messaggio Telegram, salvo --secco/--no-telegram.
|
||||
Scrive un solo file: docs/revisioni/<data>.md."""
|
||||
|
||||
|
||||
def _arg(flag: str, default=None):
|
||||
a = sys.argv[1:]
|
||||
return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from src.live.cli import valida
|
||||
valida("revisione.py", USO, flag=("--secco", "--no-telegram", "--quiet"),
|
||||
con_valore=("--giorno", "--modello"))
|
||||
from src.live import revisione as R
|
||||
g = _arg("--giorno")
|
||||
adesso = (datetime.fromisoformat(g).replace(tzinfo=timezone.utc) if g
|
||||
else datetime.now(timezone.utc))
|
||||
esito = R.scrivi(ROOT, adesso, modello=_arg("--modello", R.MODELLO),
|
||||
secco="--secco" in sys.argv, telegram="--no-telegram" not in sys.argv,
|
||||
quiet="--quiet" in sys.argv)
|
||||
if "--quiet" in sys.argv and not esito.get("secco"):
|
||||
print(f"revisione {esito['data']}: {esito.get('stato')}"
|
||||
+ (f" — {esito.get('errore')}" if esito.get("errore") else "") + f" {esito.get('file')}")
|
||||
raise SystemExit(0 if esito.get("secco") or esito.get("stato") in ("ok", "sospetta") else 1)
|
||||
@@ -9,6 +9,9 @@ lontana, che il 30/07 aveva misurato sottoprezzata ~2.3x.
|
||||
piu' mispriced in RELATIVO (f_long 5.85 contro 2.23) ma pesa il **3.2%** del premio corto invece
|
||||
del **18.4%**, quindi sul credito NETTO l'effetto e' minore: f_net **0.852** contro **0.718**.
|
||||
Meccanismo giusto, segno della conclusione sbagliato.
|
||||
(Numeri del 30/07, misurati con `cblib` che leggeva spot e DVOL di UN'ORA / fino a 24 ORE dopo
|
||||
l'ingresso — debito §5.18, riparato la sera del 09/09: da allora la tabella stampata da questo
|
||||
script e' la fonte, e il f canonico e' passato da 0,732 a 0,706 sulle stesse 28 coppie.)
|
||||
|
||||
E' pero' una misura su **16 coppie**: la differenza appaiata e' **+0.109 con IC95
|
||||
[-0.047, +0.193]**, cioe' **compatibile con zero**. Serve piu' campione, e "quando ce ne sara'
|
||||
|
||||
@@ -13,6 +13,10 @@ Regola che vale per ogni misura fatta con questo modulo: il f della sola gamba c
|
||||
dello spread. Il modello sbaglia soprattutto sull'ala che si COMPRA (piu' OTM, IV piu' alta per
|
||||
skew, prezzata dal modello a vol ATM) -> misurare una gamba sola da' la risposta sbagliata con
|
||||
segno rassicurante.
|
||||
|
||||
Seconda regola (09/09, debito §5.18): spot e DVOL escono da qui gia' CAUSALI — `asof(ts)` da'
|
||||
l'ultima chiusura nota a ts. Chi costruisce una serie di prezzi altrove usi `causale()`, non
|
||||
l'indice del feed com'e' (etichettato all'apertura: D6).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -65,23 +69,48 @@ def puts(asset: str, df: pd.DataFrame | None = None) -> pd.DataFrame:
|
||||
return d[(d["asset"] == asset) & (d["option_type"] == "P")]
|
||||
|
||||
|
||||
def causale(s: pd.Series, cadenza: str) -> pd.Series:
|
||||
"""Rietichetta una serie di CHIUSURE dall'APERTURA della barra all'istante in cui la chiusura
|
||||
e' nota (apertura + cadenza), cosi' `s.asof(ts)` restituisce l'ultima chiusura CONOSCIUTA a ts
|
||||
e mai una futura. I valori non si toccano, solo le etichette.
|
||||
|
||||
🚨 Perche' esiste (debito §5.18, 09/09): i feed del progetto sono etichettati all'apertura —
|
||||
la barra 1h `20:00` chiude col 5m delle 20:55 (verificato: chiusura 1h a T == chiusura 5m a
|
||||
T+55 nel 100% delle barre) e la riga DVOL del giorno D e' la chiusura delle 23:00 di D
|
||||
(verificato contro la risoluzione 1h dell'API pubblica). Senza questo spostamento `asof(ts)`
|
||||
dava lo spot di UN'ORA dopo e il DVOL di FINO A 24 ORE dopo l'ingresso (D6, quinta occorrenza)."""
|
||||
out = s.copy()
|
||||
out.index = out.index + pd.Timedelta(cadenza)
|
||||
return out
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def spot_series(asset: str) -> pd.Series:
|
||||
"""Chiusure 1h del feed certificato, etichettate all'istante in cui sono NOTE (apertura + 1h).
|
||||
|
||||
`spot_series(a).asof(ts)` = ultima chiusura oraria conosciuta a `ts`. Conseguenza sul
|
||||
regolamento: `asof(exp)` alle 08:00 e' la chiusura della barra 07:00, cioe' il prezzo delle
|
||||
08:00 — prima era il prezzo delle 09:00, un'ora DOPO la scadenza."""
|
||||
from scripts.analysis.research_lab import load_tf
|
||||
px = load_tf(asset, "1h")
|
||||
s = pd.Series(px["close"].values.astype(float),
|
||||
index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index()
|
||||
s.index = pd.DatetimeIndex(s.index).as_unit("ns") # le quote bite hanno microsecondi
|
||||
return s
|
||||
return causale(s, "1h")
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def dvol_series(asset: str) -> pd.Series:
|
||||
"""DVOL giornaliero (chiusura), etichettato all'istante in cui e' NOTO (giorno + 1D).
|
||||
|
||||
`dvol_series(a).asof(ts)` = ultima chiusura giornaliera del DVOL conosciuta a `ts`: dentro il
|
||||
giorno D e' la chiusura di D−1, quindi vecchia fino a 23 ore. E' il prezzo della causalita'
|
||||
con un feed giornaliero; l'alternativa (DVOL orario dall'API pubblica) non e' nel feed."""
|
||||
d = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
|
||||
s = pd.Series(d["close"].values.astype(float),
|
||||
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
|
||||
s.index = pd.DatetimeIndex(s.index).as_unit("ns")
|
||||
return s
|
||||
return causale(s, "1D")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ prezzo
|
||||
|
||||
@@ -310,11 +310,15 @@ def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--no-live", action="store_true", help="non interrogare il venue")
|
||||
ap.add_argument("--al", default=None,
|
||||
help="data di taglio UTC (riproduzione): esclude le scadenze >= questa data; default adesso")
|
||||
args = ap.parse_args()
|
||||
|
||||
print(__doc__.split("\n\n")[0])
|
||||
puts = load_puts()
|
||||
today = pd.Timestamp.now(tz="UTC")
|
||||
today = pd.Timestamp(args.al, tz="UTC") if args.al else pd.Timestamp.now(tz="UTC")
|
||||
print(f"\n taglio dei trade: scadenze < {today:%Y-%m-%d %H:%M}Z"
|
||||
+ (" (--al: riproduzione)" if args.al else " (adesso)"))
|
||||
venue = venue_read(not args.no_live)
|
||||
|
||||
# ------------------------------------------------------------ §1 REGIME
|
||||
@@ -323,7 +327,7 @@ def main() -> None:
|
||||
"misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n"
|
||||
"la strategia — contiene la sua astensione.\n")
|
||||
for a in ASSETS:
|
||||
V = CB.dvol_series(a)
|
||||
V = CB.causale(CB.dvol_series(a), "-1D") # calendario: il DVOL DEL giorno D (la serie di cblib e' causale: sta a D+1)
|
||||
win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())]
|
||||
hist = V[V.index < win.index[0]].to_numpy(float)
|
||||
print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / "
|
||||
@@ -372,7 +376,7 @@ def main() -> None:
|
||||
print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n"
|
||||
" numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):")
|
||||
for a in ASSETS:
|
||||
V = CB.dvol_series(a)
|
||||
V = CB.causale(CB.dvol_series(a), "-1D") # idem: date di calendario, non etichette causali
|
||||
vals = V.to_numpy(float)
|
||||
ivr = np.full(len(vals), np.nan)
|
||||
for i in range(1, len(vals)):
|
||||
@@ -753,13 +757,17 @@ def main() -> None:
|
||||
|
||||
# ------------------------------------------------------------ §7 VERDETTO
|
||||
hr("§7 VERDETTO")
|
||||
print(""" SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
|
||||
print(""" (numeri della stesura del 22/08, RIMISURATI il 09/09 con spot e DVOL causali — debito §5.18:
|
||||
erano 0.714 [0.690, 0.779] · 32.58 / 1.45 / -0.35 · 3.90 -> 2.41 · -1.35% -> +11.22% · +25% / +44%.
|
||||
Il verdetto non cambia: le quattro ragioni non dipendono da un'ora di spot.)
|
||||
|
||||
SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
|
||||
l'uno dall'altro: ciascuno da solo basterebbe.
|
||||
|
||||
1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30
|
||||
che E' l'alpha di VRP01 — il sleeve sarebbe stato FLAT per l'intero campione. La DVOL
|
||||
mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e'
|
||||
salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE
|
||||
salito del +21% / +40%: vol implicita bassa E rialzo forte = il regime MIGLIORE
|
||||
possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16
|
||||
(prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE
|
||||
non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la
|
||||
@@ -770,13 +778,13 @@ def main() -> None:
|
||||
regime di vol bassa persistente, non davanti a un muro.
|
||||
|
||||
2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato
|
||||
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58
|
||||
sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35).
|
||||
Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58.
|
||||
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 29.61
|
||||
sta all'89 percentile, la MEDIANA ONESTA e' 2.12 e la banda tocca il NEGATIVO (-0.19).
|
||||
Su ETH 7.64 -> 3.35. Il numero da citare per BTC e' 2.12, non 29.61.
|
||||
⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la
|
||||
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a
|
||||
+11.22% del rischio — CAMBIA SEGNO — e la deviazione standard di 11x (ETH: media 2.9x,
|
||||
sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
||||
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -0.66% a
|
||||
+11.44% del rischio — CAMBIA SEGNO — e la deviazione standard di 9.2x (ETH: media 2.1x,
|
||||
sd 3.2x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
||||
col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10
|
||||
settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare.
|
||||
|
||||
@@ -793,7 +801,7 @@ def main() -> None:
|
||||
4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare
|
||||
il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo
|
||||
spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest
|
||||
~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779],
|
||||
~0. Il f del credito netto si replica indipendentemente a 0.712 (IC95 [0.664, 0.732],
|
||||
0/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e
|
||||
con un percorso diverso.
|
||||
|
||||
|
||||
@@ -167,7 +167,8 @@ def calibra_catena() -> dict:
|
||||
skew[nm] = (np.array(xs), np.array(ys))
|
||||
|
||||
# struttura a termine: IV ATM a DTE t contro il DVOL 30g dello stesso istante
|
||||
dv = cblib.dvol_series("BTC")
|
||||
dv = cblib.causale(cblib.dvol_series("BTC"), "-1D") # join per GIORNO: il DVOL del giorno D — la serie di
|
||||
# cblib e' causale dal 09/09 sera (la chiusura di D sta a D+1); senza questo il join accosterebbe l'IV di D al DVOL di D−1
|
||||
a = q[q["ad"].between(0.45, 0.55)].copy()
|
||||
a["giorno"] = a["ts"].dt.floor("D")
|
||||
dvd = dv.copy()
|
||||
|
||||
@@ -33,8 +33,10 @@ Un episodio e' un episodio: da' la TAGLIA di f nello stress, non la sua distribu
|
||||
|
||||
DUE CORREZIONI DELLA REVISIONE (fable, 09/09), applicate:
|
||||
* il feed 1h e' etichettato all'APERTURA (la barra 20:00 chiude alle 21:00): `S.asof(ts)`
|
||||
guardava un'ora AVANTI. Qui lo spot e' `spot_causale` = chiusura dell'ultima barra chiusa a ts.
|
||||
⚠️ `cblib.spot_series` ha lo stesso difetto (e quindi il f 0,714 di §11): registrato, non toccato.
|
||||
guardava un'ora AVANTI. Corretto la mattina del 09/09 qui con uno `spot_causale` locale;
|
||||
la sera dello stesso giorno la riparazione e' entrata in `cblib` (`causale`, debito §5.18) e
|
||||
copre anche il DVOL giornaliero (fino a 24 ore avanti): questo script usa le serie di `cblib`
|
||||
COME SONO, senza ri-spostarle.
|
||||
* il costo di chiusura `(ask corta − bid lunga)·spot` puo' superare la LARGHEZZA dello spread
|
||||
(una quota da 158% di spread relativo a T→0): uno spread a rischio definito non costa mai piu'
|
||||
della larghezza per chiudere (si lascia regolare). Il MTM e' tagliato alla larghezza e le ore
|
||||
@@ -76,13 +78,6 @@ def leg_fee(prem_base: float, spot: float) -> float:
|
||||
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot)
|
||||
|
||||
|
||||
def spot_causale(S: pd.Series) -> pd.Series:
|
||||
"""Il feed 1h e' etichettato all'apertura: la chiusura della barra T e' nota a T+1h. Spostando
|
||||
l'indice di +1h, `asof(ts)` restituisce l'ultima chiusura CONOSCIUTA a ts, mai una futura."""
|
||||
out = S.copy(); out.index = out.index + pd.Timedelta(hours=1)
|
||||
return out
|
||||
|
||||
|
||||
def carica() -> pd.DataFrame:
|
||||
a = pd.read_parquet(ARCHIVIO)
|
||||
a["ts"] = pd.to_datetime(a["ts"], utc=True)
|
||||
@@ -213,7 +208,7 @@ def misura() -> dict:
|
||||
out = {"trades": {}, "contesto": {}, "sleeve": {}}
|
||||
for asset in ASSETS:
|
||||
seg = a[a["asset"] == asset]
|
||||
S, V = spot_causale(CB.spot_series(asset)), CB.dvol_series(asset)
|
||||
S, V = CB.spot_series(asset), CB.dvol_series(asset) # gia' causali (cblib, dal 09/09 sera)
|
||||
Q = Quote(seg, S, V)
|
||||
rows = []
|
||||
for ts in sorted(seg["ts"].unique()):
|
||||
@@ -227,17 +222,21 @@ def misura() -> dict:
|
||||
# contesto: DVOL, RV5 e RV1 sul crollo
|
||||
px = S[(S.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (S.index <= EXP)]
|
||||
pd_ = px.resample("1D").last().dropna() # chiusura del giorno = ultima chiusura nota entro le 24:00
|
||||
hist_all = V[V.index < CROLLO[0]]
|
||||
# CONTESTO per giorno: la serie di cblib e' causale (la chiusura del giorno D sta a D+1);
|
||||
# qui si vuole "il DVOL DEL giorno D" accanto al prezzo del giorno D, quindi si toglie lo
|
||||
# spostamento SOLO per la tabella e per il picco del crollo. `dvol_pre` e `ivr_pre` restano
|
||||
# sulla serie causale: sono cio' che si sapeva alle 00:00 del primo giorno di crollo.
|
||||
dvg = CB.causale(V, "-1D")
|
||||
hist_all = dvg[dvg.index < CROLLO[0]]
|
||||
lr = np.log(pd_).diff()
|
||||
rv5 = lr.rolling(5).std() * np.sqrt(365.25) * 100
|
||||
dv = V[(V.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (V.index <= EXP)]
|
||||
hist = V[V.index < CROLLO[0]]
|
||||
dv = dvg[(dvg.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (dvg.index <= EXP)]
|
||||
out["contesto"][asset] = dict(
|
||||
spot=pd_, ret=lr, rv5=rv5, dvol=dv,
|
||||
dvol_max_crollo=float(dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()),
|
||||
dvol_pre=float(V.asof(CROLLO[0])),
|
||||
pctl_picco=float((hist_all < V[(V.index >= CROLLO[0]) & (V.index <= CROLLO[1])].max()).mean()),
|
||||
ivr_pre=float((hist < V.asof(CROLLO[0])).mean()),
|
||||
pctl_picco=float((hist_all < dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()).mean()),
|
||||
ivr_pre=float((hist_all < V.asof(CROLLO[0])).mean()),
|
||||
rv5_max=float(rv5[(rv5.index >= CROLLO[0]) & (rv5.index <= CROLLO[1] + pd.Timedelta(days=3))].max()),
|
||||
caduta=float(pd_[CROLLO[0]:CROLLO[1]].min() / pd_[:CROLLO[0]].iloc[-1] - 1.0),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
"""Revisione settimanale in SOLA LETTURA: un revisore diverso dall'autore rilegge il sistema.
|
||||
|
||||
PERCHE' ESISTE (2026-09-09). Nove sorveglianti in cron guardano ognuno la propria grandezza
|
||||
(venue, fee, edge, scala, USDE, VRP-f, health, balance, paper-monitor); il giornale e l'analista
|
||||
leggono UN giorno. Nessuno rilegge il sistema nel suo insieme: stato, gate con le date, decisioni
|
||||
vincolanti, debiti aperti, cosa e' cambiato nei diari e nei commit. La revisione con un secondo
|
||||
modello (regola del 2026-09-03: chi rivede non e' chi ha scritto) ha trovato in una sessione due
|
||||
conclusioni false e un look-ahead che l'autore aveva riletto tre volte. Questo modulo la schedula.
|
||||
|
||||
COSA FA. Raccoglie il materiale (CLAUDE.md, config, crontab, log git, monitor_health, giornale
|
||||
degli ultimi giorni, diari recenti, stato dei sorveglianti), lo da' a un modello DIVERSO da quello
|
||||
che scrive il codice, e salva il rapporto FIRMATO in `docs/revisioni/<data>.md`, con una sintesi su
|
||||
Telegram. Il rapporto e' l'opinione di un lettore fallibile (P13): dice cosa non torna, cosa e'
|
||||
maturo da decidere, cosa propone. Non e' parte del registro delle misure.
|
||||
|
||||
COSA NON FA, PER COSTRUZIONE. Non tocca config, libro, cron, codice, dati: l'unico file che
|
||||
scrive e' il rapporto. Non esegue proposte: le scrive, e l'operatore decide. Non riapre le
|
||||
decisioni vincolanti (CLAUDE.md §3) se la colonna «cosa la riapre» non e' soddisfatta, e non
|
||||
propone «idee nuove» senza citare il file di memoria che ha gia' ucciso l'idea simile. Un ciclo
|
||||
che si autoregola e' esattamente cio' che `scale_watch` e `venue_watch` trattano come anomalia.
|
||||
|
||||
Il silenzio non e' una revisione: se il modello non risponde, il rapporto lo dice e Telegram lo
|
||||
dice, invece di lasciare un lunedi' vuoto che si legge come «tutto bene».
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from src.live import analista as A
|
||||
from src.live import monitor_health as MH
|
||||
|
||||
MODELLO = "claude-fable-5-1" # il revisore: NON il modello che scrive il codice
|
||||
CADENZA = "settimanale"
|
||||
FINESTRA_GIORNALE_G = 7 # pagine del giornale lette
|
||||
FINESTRA_DIARI_G = 7 # diari letti (la settimana: e' una revisione settimanale)
|
||||
FINESTRA_COMMIT_G = 14 # commit letti
|
||||
CARTELLA = Path("docs") / "revisioni"
|
||||
MAX_CHARS_VOCE = 90_000 # CLAUDE.md sta sotto; il resto e' molto piu' piccolo
|
||||
MAX_CHARS_TOTALE = 400_000
|
||||
MAX_NUMERI_LIBERI = 12 # una revisione confronta e sottrae: qualche numero suo e' lecito
|
||||
LIMITE_TELEGRAM = 3_500 # sotto i 4.096 di Telegram, con la testata
|
||||
TIMEOUT_S = 900 # il materiale e' grande: 5 minuti non bastano sempre
|
||||
|
||||
TITOLI = ("## Sintesi", "## Cosa non torna", "## Cosa e' maturo da decidere",
|
||||
"## Proposte (nessuna azione eseguita)", "## Cosa ho letto e cosa mi mancava")
|
||||
|
||||
ISTRUZIONI = """Sei il REVISORE settimanale di un progetto di trading quantitativo (BTC/ETH su Deribit, libro
|
||||
live con soldi veri, ~$4.500). Sei un modello DIVERSO da quello che scrive codice e analisi del
|
||||
progetto: il tuo valore e' che non condividi il suo ragionamento. Leggi il materiale qui sotto e
|
||||
scrivi la revisione, in italiano, per l'operatore (che decide) e per l'agente (che esegue).
|
||||
|
||||
REGOLE VINCOLANTI:
|
||||
1. SOLA LETTURA. Non proponi comandi da eseguire ora, non scrivi codice: descrivi cosa non torna
|
||||
e cosa andrebbe verificato o deciso, e da chi.
|
||||
2. OGNI SEGNALAZIONE CITA LA FONTE: file e sezione (es. «CLAUDE.md §4, gate STATARB»), data della
|
||||
pagina di giornale, nome del diario, hash del commit. Una segnalazione senza fonte non vale.
|
||||
3. USA SOLO I NUMERI DEL MATERIALE. Puoi sottrarre e confrontare, ma ogni cifra che scrivi deve
|
||||
essere rintracciabile qui sotto; un controllo automatico elenchera' quelle che non lo sono.
|
||||
4. LE DECISIONI VINCOLANTI (CLAUDE.md §3) NON SI RIPROPONGONO. Puoi dire che una condizione della
|
||||
colonna «cosa la riapre» sembra soddisfatta, citando il dato; non puoi consigliare di cambiare
|
||||
la decisione per altri motivi.
|
||||
5. UN'IDEA «NUOVA» SI PROPONE SOLO citando il file di memoria (docs/memory/…) o il paragrafo che ha
|
||||
gia' ucciso l'idea piu' simile, e dicendo in che cosa questa differisce. Se non lo sai, non la
|
||||
proponi: scrivi «da verificare in memoria prima di proporla».
|
||||
6. I GATE HANNO UNA DATA (CLAUDE.md §4). Elenca quelli la cui data cade entro 30 giorni o e' gia'
|
||||
passata, con il criterio scritto e lo stato che il materiale mostra. Non anticipare un verdetto:
|
||||
un gate si decide alla sua data, coi criteri scritti prima.
|
||||
7. DISTINGUI misurato da dedotto. «Non determinabile da questo materiale» e' una risposta buona.
|
||||
8. NIENTE PREVISIONI di mercato, niente consigli di direzione, niente target.
|
||||
9. Se qualcosa richiede l'operatore SUBITO (fondi, credenziali, un sorvegliante fermo che copre un
|
||||
gate, una modifica di config non registrata), mettilo nella Sintesi con la parola URGENTE.
|
||||
10. Massimo ~1.000 parole. Usa ESATTAMENTE questi titoli, in quest'ordine, e nient'altro prima:
|
||||
%s
|
||||
Sotto «Cosa ho letto e cosa mi mancava» di' quali fonti hai usato e quale dato ti sarebbe
|
||||
servito e non c'era (e' l'elenco da cui il materiale della prossima settimana migliora).
|
||||
""" % "\n ".join(TITOLI)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- materiale
|
||||
|
||||
def _tronca(testo: str, n: int | None = None) -> tuple[str, bool]:
|
||||
n = MAX_CHARS_VOCE if n is None else n # letto a runtime: un default e' catturato alla definizione (debito 12)
|
||||
if len(testo) <= n:
|
||||
return testo, False
|
||||
return testo[:n] + f"\n[… troncato a {n:,} caratteri su {len(testo):,}]", True
|
||||
|
||||
|
||||
def _cmd(args: list[str], cwd: Path) -> str:
|
||||
"""Uscita di un comando di SOLA LETTURA, o il motivo per cui non si legge (P5: «non vedo»
|
||||
non e' «va tutto bene»)."""
|
||||
try:
|
||||
p = subprocess.run(args, cwd=str(cwd), capture_output=True, text=True, timeout=60)
|
||||
except Exception as exc: # noqa: BLE001 — un comando di lettura che esplode si dichiara
|
||||
return f"non leggibile: {type(exc).__name__}: {str(exc)[:120]}"
|
||||
if p.returncode != 0:
|
||||
return f"non leggibile: uscita {p.returncode}: {(p.stderr or p.stdout).strip()[:200]}"
|
||||
return p.stdout
|
||||
|
||||
|
||||
def _coda(path: Path, n: int) -> str:
|
||||
if not path.exists():
|
||||
return "(assente)"
|
||||
righe = path.read_text().splitlines()
|
||||
return "\n".join(righe[-n:]) if righe else "(vuoto)"
|
||||
|
||||
|
||||
def _data_nel_nome(p: Path) -> date | None:
|
||||
try:
|
||||
return date.fromisoformat(p.name[:10])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _monitor_health(root: Path, adesso: datetime) -> str:
|
||||
try:
|
||||
righe = MH.check_all(root, now=adesso)
|
||||
except Exception as exc: # noqa: BLE001 — si dichiara, non si tace (P3)
|
||||
return f"non leggibile: {type(exc).__name__}: {exc}"
|
||||
out = ["monitor | stato | perche' | gate a rischio"]
|
||||
for r in righe:
|
||||
out.append(f"{r.get('name')} | {r.get('status')} | {r.get('why')} | {r.get('gate') or '-'}")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def materiale(root: Path, adesso: datetime) -> list[tuple[str, str, bool]]:
|
||||
"""Le voci (nome, testo, troncato) date al revisore. Tutto in SOLA LETTURA.
|
||||
|
||||
L'ordine e' quello in cui un lettore umano le aprirebbe: prima le regole e lo stato dichiarato,
|
||||
poi cio' che gira (sorveglianti: piccoli, e per questo PRIMA delle voci grandi, cosi' il tetto
|
||||
totale non li taglia mai), poi cio' che e' successo (giornale, diari).
|
||||
"""
|
||||
oggi = adesso.date()
|
||||
voci: list[tuple[str, str]] = []
|
||||
|
||||
voci.append(("CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole)",
|
||||
(root / "CLAUDE.md").read_text() if (root / "CLAUDE.md").exists() else "(assente)"))
|
||||
cfg = root / "config" / "live.json"
|
||||
voci.append(("config/live.json (unica autorita' dei guardrail live)",
|
||||
cfg.read_text() if cfg.exists() else "(assente)"))
|
||||
voci.append(("crontab installata (crontab -l)", _cmd(["crontab", "-l"], root)))
|
||||
since = (oggi - timedelta(days=FINESTRA_COMMIT_G)).isoformat()
|
||||
voci.append((f"git log dal {since} + stato del working tree",
|
||||
_cmd(["git", "log", f"--since={since}", "--format=%h %ad %s", "--date=short"], root)
|
||||
+ "\n--- git status --short ---\n" + _cmd(["git", "status", "--short"], root)))
|
||||
voci.append(("monitor_health (i forward-monitor stanno registrando?)", _monitor_health(root, adesso)))
|
||||
|
||||
live = root / "data" / "live"
|
||||
stati = {
|
||||
"scale_watch_state.json": (live / "scale_watch_state.json", 999),
|
||||
"scale_history.jsonl (ultime 3)": (live / "scale_history.jsonl", 3),
|
||||
"vrp_f_watch/state.json": (root / "data" / "vrp_f_watch" / "state.json", 999),
|
||||
"usde_watch.jsonl (ultime 3)": (live / "usde_watch.jsonl", 3),
|
||||
"usde_convert.jsonl (ultime 3)": (live / "usde_convert.jsonl", 3),
|
||||
"balance_watch.jsonl (ultime 2)": (live / "balance_watch.jsonl", 2),
|
||||
"venue_news.jsonl (ultime 5)": (live / "venue_news.jsonl", 5),
|
||||
"movimenti_dichiarati.jsonl": (live / "movimenti_dichiarati.jsonl", 50),
|
||||
"equity_seen.json (watermark)": (live / "equity_seen.json", 999),
|
||||
}
|
||||
for nome, (p, n) in stati.items():
|
||||
voci.append((f"stato {nome}", _coda(p, n)))
|
||||
|
||||
for i in range(FINESTRA_GIORNALE_G, 0, -1):
|
||||
g = oggi - timedelta(days=i)
|
||||
p = root / "docs" / "journal" / f"{g.isoformat()}.md"
|
||||
if p.exists():
|
||||
voci.append((f"giornale {g.isoformat()}", p.read_text()))
|
||||
diari = sorted(p for p in (root / "docs" / "diary").glob("*.md")
|
||||
if (_data_nel_nome(p) or date.min) >= oggi - timedelta(days=FINESTRA_DIARI_G))
|
||||
for p in diari:
|
||||
voci.append((f"diario {p.name}", p.read_text()))
|
||||
|
||||
out, tot = [], 0
|
||||
for nome, testo in voci:
|
||||
t, tr = _tronca(testo)
|
||||
if tot + len(t) > MAX_CHARS_TOTALE:
|
||||
t, tr = _tronca(t, max(0, MAX_CHARS_TOTALE - tot)), True
|
||||
tot += len(t)
|
||||
out.append((nome, t, tr))
|
||||
return out
|
||||
|
||||
|
||||
def costruisci_prompt(voci: list[tuple[str, str, bool]], adesso: datetime) -> str:
|
||||
parti = [ISTRUZIONI, f"\nDATA DELLA REVISIONE: {adesso:%Y-%m-%d %H:%M}Z (usa questa per «oggi» e per le date dei gate).\n"]
|
||||
for nome, testo, tr in voci:
|
||||
parti.append(f"\n=== {nome}{' [TRONCATO]' if tr else ''} ===\n{testo}\n")
|
||||
parti.append("\nScrivi ora la revisione, coi cinque titoli, e nient'altro.")
|
||||
return "\n".join(parti)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- guardia
|
||||
|
||||
def valida(testo: str, fonte: str) -> tuple[str, list[str]]:
|
||||
"""(stato, motivi) — stato in {ok, sospetta, rifiutata}. Rifiutata = non e' una revisione
|
||||
(vuota o senza i titoli); sospetta = numeri non rintracciabili nel materiale, elencati."""
|
||||
motivi: list[str] = []
|
||||
t = testo.strip()
|
||||
if not t:
|
||||
return "rifiutata", ["risposta vuota"]
|
||||
mancanti = [x for x in TITOLI if x not in t]
|
||||
if mancanti:
|
||||
return "rifiutata", [f"titoli mancanti: {', '.join(mancanti)}"]
|
||||
fuori = A.numeri_non_supportati(t, fonte)
|
||||
if fuori:
|
||||
motivi.append(f"{len(fuori)} numeri non presenti nel materiale: " + ", ".join(fuori[:30]))
|
||||
if len(fuori) > MAX_NUMERI_LIBERI:
|
||||
return "sospetta", motivi
|
||||
return ("sospetta" if motivi else "ok"), motivi
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- rapporto
|
||||
|
||||
def rapporto(testo: str | None, errore: str | None, stato: str, motivi: list[str], modello: str,
|
||||
adesso: datetime, voci: list[tuple[str, str, bool]]) -> str:
|
||||
testa = [f"# Revisione {CADENZA} — {adesso:%Y-%m-%d}", ""]
|
||||
if errore:
|
||||
testa += [f"## Revisione NON eseguita",
|
||||
"",
|
||||
f"Il modello `{modello}` non ha risposto: **{errore}**. Non c'e' una revisione questa "
|
||||
"settimana, e questo file esiste perche' un lunedi' senza rapporto si leggerebbe "
|
||||
"come «tutto bene» (P5).", ""]
|
||||
corpo = []
|
||||
else:
|
||||
testa += [f"*Scritta il {adesso:%Y-%m-%dT%H:%M:%S}Z dal modello `{modello}`, revisore diverso "
|
||||
"dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun "
|
||||
"ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e "
|
||||
"ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: "
|
||||
f"**{stato}**" + (f" — {'; '.join(motivi)}" if motivi else "") + ".*", ""]
|
||||
corpo = [testo.strip(), ""]
|
||||
coda = ["---", "## Firma e materiale", "",
|
||||
f"- revisore: `{modello}` · cadenza {CADENZA} · giornale {FINESTRA_GIORNALE_G} g · "
|
||||
f"diari {FINESTRA_DIARI_G} g · commit {FINESTRA_COMMIT_G} g",
|
||||
"- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file "
|
||||
"scritto e' questo", "",
|
||||
"| voce | caratteri | troncata |", "|---|---|---|"]
|
||||
coda += [f"| {n} | {len(t):,} | {'si' if tr else ''} |" for n, t, tr in voci]
|
||||
return "\n".join(testa + corpo + coda) + "\n"
|
||||
|
||||
|
||||
def _sezione(testo: str, titolo: str) -> str:
|
||||
if titolo not in testo:
|
||||
return ""
|
||||
dopo = testo.split(titolo, 1)[1]
|
||||
for t in TITOLI:
|
||||
if t != titolo and t in dopo:
|
||||
dopo = dopo.split(t, 1)[0]
|
||||
return dopo.strip()
|
||||
|
||||
|
||||
def per_telegram(testo: str | None, errore: str | None, stato: str, adesso: datetime,
|
||||
path: Path) -> str:
|
||||
"""La Sintesi, con la testata e il taglio DICHIARATO (un taglio silenzioso e' peggio di
|
||||
un'assenza: si legge come un pensiero finito)."""
|
||||
if errore:
|
||||
return (f"<b>🚨 Revisione {CADENZA} NON eseguita</b> {adesso:%Y-%m-%d}\n"
|
||||
f"{html.escape(errore)}\n<code>{html.escape(str(path))}</code>")
|
||||
sintesi = _sezione(testo or "", TITOLI[0]) or "(nessuna Sintesi nel rapporto)"
|
||||
corpo = html.escape(sintesi, quote=False)
|
||||
tagliato = ""
|
||||
if len(corpo) > LIMITE_TELEGRAM:
|
||||
corpo, tagliato = corpo[:LIMITE_TELEGRAM], "\n[… sintesi tagliata: il resto e' nel file]"
|
||||
return (f"<b>🔍 Revisione {CADENZA}</b> {adesso:%Y-%m-%d} · guardia numeri: {stato}\n\n"
|
||||
f"{corpo}{tagliato}\n\n<code>{html.escape(str(path))}</code>")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- modello
|
||||
|
||||
def interroga(prompt: str, modello: str = MODELLO, timeout: int = TIMEOUT_S) -> tuple[str, str | None]:
|
||||
"""(testo, errore) dalla CLI `claude -p`, col prompt su STDIN.
|
||||
|
||||
Non su argv: al primo giro reale (09/09, 195k caratteri) `analista.interroga` e' morto con
|
||||
`OSError: Argument list too long` PRIMA di chiamare il modello — e l'eccezione, non catturata,
|
||||
ha lasciato zero rapporto e zero Telegram: il silenzio che questo modulo esiste per vietare.
|
||||
Qui ogni eccezione della chiamata e' un errore RESTITUITO (P3), mai propagato.
|
||||
"""
|
||||
try:
|
||||
p = subprocess.run(
|
||||
["claude", "-p", "--model", modello, "--allowed-tools", "", "--max-turns", "1"],
|
||||
input=prompt, capture_output=True, text=True, timeout=timeout)
|
||||
except FileNotFoundError:
|
||||
return "", "CLI `claude` non trovata"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", f"timeout dopo {timeout}s"
|
||||
except Exception as exc: # noqa: BLE001 — si restituisce, non si tace
|
||||
return "", f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||
if p.returncode != 0:
|
||||
return "", A.motivo_uscita(p.returncode, p.stdout, p.stderr)
|
||||
return p.stdout.strip(), None # NON `analista.pulisci`: toglierebbe i titoli «## …» che la guardia richiede
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- orchestrazione
|
||||
|
||||
def scrivi(root: Path, adesso: datetime | None = None, modello: str = MODELLO, *,
|
||||
secco: bool = False, telegram: bool = True, quiet: bool = False,
|
||||
interroga=None, invia=None) -> dict:
|
||||
"""Un giro: materiale → modello → rapporto → Telegram. Ritorna cosa e' successo.
|
||||
|
||||
`interroga(prompt, modello, timeout) -> (testo, errore)` e `invia(html) -> bool` sono
|
||||
INIETTATI (default: `interroga` di questo modulo — prompt su stdin — e `notifier.send`) cosi'
|
||||
i test non spendono e non mandano. Se uno dei due SOLLEVA, l'eccezione diventa un errore nel
|
||||
rapporto: un giro che muore a meta' non deve lasciare un lunedi' vuoto. Con `secco` non si chiama il modello e non si scrive niente: si stampa la taglia.
|
||||
"""
|
||||
adesso = adesso or datetime.now(timezone.utc)
|
||||
voci = materiale(root, adesso)
|
||||
prompt = costruisci_prompt(voci, adesso)
|
||||
esito = {"data": adesso.date().isoformat(), "voci": len(voci), "caratteri": len(prompt),
|
||||
"modello": modello, "secco": secco}
|
||||
if not quiet:
|
||||
print(f" revisione {esito['data']} modello {modello} materiale {len(voci)} voci, "
|
||||
f"{len(prompt):,} caratteri" + (" [SECCO: nessuna chiamata, nessuna scrittura]" if secco else ""))
|
||||
for n, t, tr in voci:
|
||||
print(f" {len(t):>8,} {n}{' [troncata]' if tr else ''}")
|
||||
if secco:
|
||||
return esito
|
||||
|
||||
interroga = interroga or globals()["interroga"]
|
||||
try:
|
||||
testo, errore = interroga(prompt, modello, TIMEOUT_S)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
testo, errore = "", f"eccezione nella chiamata al modello: {type(exc).__name__}: {str(exc)[:200]}"
|
||||
stato, motivi = ("rifiutata", [errore]) if errore else valida(testo, prompt)
|
||||
if not errore and stato == "rifiutata":
|
||||
errore, testo = "risposta non e' una revisione: " + "; ".join(motivi), None
|
||||
|
||||
cartella = root / CARTELLA
|
||||
cartella.mkdir(parents=True, exist_ok=True)
|
||||
path = cartella / f"{adesso:%Y-%m-%d}.md"
|
||||
path.write_text(rapporto(testo, errore, stato, motivi, modello, adesso, voci))
|
||||
esito.update(stato=stato, errore=errore, motivi=motivi, file=str(path))
|
||||
|
||||
if telegram:
|
||||
from src.live import notifier as N
|
||||
invia = invia or (lambda h: N.send(h, tentativi=2))
|
||||
try:
|
||||
esito["telegram"] = bool(invia(per_telegram(testo, errore, stato, adesso, path)))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
esito["telegram"] = False
|
||||
esito["telegram_errore"] = f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||
if not quiet:
|
||||
tg = "" if "telegram" not in esito else (" telegram: inviato" if esito["telegram"] else
|
||||
f" telegram: NON inviato ({esito.get('telegram_errore', 'send False')})")
|
||||
print(f" -> {stato}" + (f" ({errore})" if errore else "") + f" {path}{tg}")
|
||||
return esito
|
||||
@@ -23,7 +23,8 @@ sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "analysis"))
|
||||
|
||||
from cblib import bs_put, f_factors, pick_legs # noqa: E402
|
||||
import cblib # noqa: E402
|
||||
from cblib import bs_put, causale, f_factors, pick_legs # noqa: E402
|
||||
from certify_cb_chain import ( # noqa: E402
|
||||
crossed_rate, hollow_rate, monotonicity_violations, worst_day_hollow,
|
||||
)
|
||||
@@ -196,3 +197,61 @@ def test_nel_sleeve_f_moltiplica_il_credito_NETTO():
|
||||
assert SL.VRP_CFG["f"] == 1.0, (
|
||||
"il sleeve gira ancora a f=1.0: se cambia, aggiornare il diario 2026-07-30 e CLAUDE.md"
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# C. CAUSALITA' delle serie di contesto (debito §5.18, 09/09): `asof(ts)` non guarda avanti
|
||||
# ===========================================================================================
|
||||
|
||||
def test_causale_rietichetta_alla_chiusura_e_non_tocca_i_valori():
|
||||
"""Il feed e' etichettato all'APERTURA: la chiusura della barra T e' nota a T+cadenza.
|
||||
DEVE FALLIRE SE: qualcuno "semplifica" togliendo lo spostamento, o sposta i VALORI."""
|
||||
idx = pd.date_range("2026-06-01", periods=5, freq="h", tz="UTC")
|
||||
S = pd.Series([10.0, 11.0, 12.0, 13.0, 14.0], index=idx)
|
||||
Sc = causale(S, "1h")
|
||||
assert list(Sc.index) == list(idx + pd.Timedelta(hours=1))
|
||||
assert Sc.asof(idx[2]) == 11.0, "a 02:00 e' nota la chiusura della barra 01:00"
|
||||
assert S.asof(idx[2]) == 12.0, "il difetto che la funzione ripara (controllo positivo)"
|
||||
assert np.isnan(Sc.asof(idx[0])), "prima della prima chiusura nota non c'e' spot"
|
||||
assert list(Sc.values) == list(S.values), "i VALORI non si toccano, solo le etichette"
|
||||
|
||||
|
||||
def test_spot_series_e_causale_sul_feed_1h(monkeypatch):
|
||||
"""`cblib.spot_series(a).asof(ts)` = ultima chiusura ORARIA nota a ts. Controllo positivo:
|
||||
la serie grezza del feed (etichettata all'apertura) a ts da' la chiusura di ts+1h."""
|
||||
import scripts.analysis.research_lab as rl
|
||||
idx = pd.date_range("2026-06-01", periods=6, freq="h", tz="UTC")
|
||||
epoch = pd.Timestamp("1970-01-01", tz="UTC") # NON `asi8 // 10**6`: in pandas 3 e' in `us` (D6)
|
||||
px = pd.DataFrame({"timestamp": ((idx - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"),
|
||||
"close": [100.0, 101.0, 102.0, 103.0, 104.0, 105.0]})
|
||||
monkeypatch.setattr(rl, "load_tf", lambda asset, tf: px)
|
||||
cblib.spot_series.cache_clear()
|
||||
try:
|
||||
S = cblib.spot_series("ZZZ")
|
||||
ts = idx[3] + pd.Timedelta(minutes=17) # 03:17: la barra 03:00 e' ancora aperta
|
||||
assert S.asof(ts) == 102.0, "a 03:17 l'ultima chiusura nota e' quella della barra 02:00"
|
||||
assert S.asof(idx[3]) == 102.0, "a 03:00 esatte la barra 02:00 e' appena chiusa"
|
||||
assert S.asof(idx[5] + pd.Timedelta(hours=1)) == 105.0, "l'ultima chiusura arriva un'ora dopo"
|
||||
grezza = pd.Series(px["close"].values, index=idx)
|
||||
assert grezza.asof(ts) == 103.0, "il difetto riparato: la serie del feed a 03:17 da' la chiusura delle 04:00"
|
||||
finally:
|
||||
cblib.spot_series.cache_clear()
|
||||
|
||||
|
||||
def test_dvol_series_e_causale_sul_feed_giornaliero(monkeypatch, tmp_path):
|
||||
"""La riga DVOL del giorno D e' la chiusura delle 23:00 di D: dentro D non e' ancora nota.
|
||||
`cblib.dvol_series(a).asof(ts in D)` deve dare la chiusura di D−1."""
|
||||
days = pd.date_range("2026-06-01", periods=4, freq="D", tz="UTC")
|
||||
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||
pd.DataFrame({"timestamp": ((days - epoch) // pd.Timedelta(milliseconds=1)).astype("int64"),
|
||||
"close": [36.0, 37.0, 43.0, 47.0]}).to_parquet(tmp_path / "dvol_zzz.parquet")
|
||||
monkeypatch.setattr(cblib, "RAW", tmp_path)
|
||||
cblib.dvol_series.cache_clear()
|
||||
try:
|
||||
V = cblib.dvol_series("ZZZ")
|
||||
ts = days[2] + pd.Timedelta(hours=10) # 03/06 10:00
|
||||
assert V.asof(ts) == 37.0, "dentro il 03/06 l'ultimo DVOL noto e' la chiusura del 02/06"
|
||||
assert V.asof(days[3]) == 43.0, "alle 00:00 del 04/06 e' nota la chiusura del 03/06"
|
||||
assert np.isnan(V.asof(days[0] + pd.Timedelta(hours=5))), "nel primo giorno non c'e' storia"
|
||||
finally:
|
||||
cblib.dvol_series.cache_clear()
|
||||
|
||||
@@ -8,7 +8,7 @@ numero con quel peso ha bisogno di guardie sul MECCANISMO, non solo sull'esito:
|
||||
nei casi degeneri (T≤0, sig=0 → intrinseco): se diverge, il denominatore di ogni `f` e' un
|
||||
altro modello e il rapporto non significa piu' niente;
|
||||
* **lo spot dev'essere CAUSALE.** Il feed 1h e' etichettato all'APERTURA: la barra 20:00 chiude
|
||||
alle 21:00, quindi `S.asof(ts)` guardava un'ora AVANTI. `spot_causale` sposta l'indice di
|
||||
alle 21:00, quindi `S.asof(ts)` guardava un'ora AVANTI. `cblib.causale` sposta l'indice di
|
||||
+1h. E' un look-ahead di un'ora dentro una misura di stress — piccolo e letale;
|
||||
* **il costo di chiusura si TAGLIA alla larghezza.** Uno spread a rischio definito non costa
|
||||
mai piu' della larghezza per chiudersi: si lascia regolare. Senza il taglio il "peggior MTM"
|
||||
@@ -96,25 +96,28 @@ def _trade(d_short: float, d_long: float, bid_s=0.030, ask_s=0.032, bid_l=0.010,
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# spot_causale — il look-ahead di un'ora
|
||||
# lo spot e il DVOL arrivano da cblib GIA' causali: qui non si ri-spostano (doppio shift = un'ora
|
||||
# di RITARDO invece di un'ora di anticipo, stesso errore col segno opposto)
|
||||
# ===========================================================================================
|
||||
|
||||
def test_spot_causale_restituisce_l_ultima_chiusura_CONOSCIUTA():
|
||||
"""🚨 Il feed 1h e' etichettato all'APERTURA: la barra 01:00 chiude alle 02:00. Senza lo
|
||||
spostamento, `asof(02:00)` restituiva la chiusura delle 02:00 — cioe' un'ora di futuro
|
||||
dentro un ingresso. DEVE FALLIRE SE: qualcuno "semplifica" togliendo lo shift.
|
||||
|
||||
⚠️ `cblib.spot_series` ha ancora il difetto (e quindi il f 0,714 di §11): dichiarato nel
|
||||
docstring dello script, non toccato qui."""
|
||||
idx = pd.date_range("2026-06-01", periods=5, freq="h", tz="UTC")
|
||||
S = pd.Series([10.0, 11.0, 12.0, 13.0, 14.0], index=idx)
|
||||
Sc = C.spot_causale(S)
|
||||
|
||||
assert list(Sc.index) == list(idx + pd.Timedelta(hours=1))
|
||||
assert Sc.asof(idx[2]) == 11.0, "a 02:00 e' nota la chiusura della barra 01:00"
|
||||
assert S.asof(idx[2]) == 12.0, "il difetto che la funzione ripara (controllo positivo)"
|
||||
assert np.isnan(Sc.asof(idx[0])), "prima della prima chiusura nota non c'e' spot, non c'e' 10.0"
|
||||
assert Sc.iloc[0] == 10.0 and S.iloc[0] == 10.0, "i VALORI non si toccano, solo le etichette"
|
||||
def test_lo_script_usa_le_serie_di_cblib_senza_ri_spostarle():
|
||||
"""🚨 Il 09/09 mattina lo script riparava da solo il look-ahead con `spot_causale`; la sera la
|
||||
riparazione e' entrata in `cblib.spot_series` / `dvol_series` (debito §5.18). Applicare lo
|
||||
spostamento DUE volte darebbe la chiusura di due ore prima. DEVE FALLIRE SE: qualcuno
|
||||
reintroduce uno shift locale, o `cblib` smette di essere causale."""
|
||||
import inspect
|
||||
import cblib
|
||||
src = inspect.getsource(C.misura)
|
||||
assert "CB.spot_series(asset)" in src and "CB.dvol_series(asset)" in src
|
||||
assert "spot_causale" not in src and "Timedelta(hours=1)" not in src
|
||||
# l'UNICO `causale(` ammesso in misura() e' quello del contesto, e toglie un giorno al DVOL per la
|
||||
# tabella di calendario: un `causale(CB.spot_series(...), "1h")` sarebbe il doppio shift
|
||||
assert src.count("causale(") == 1 and 'CB.causale(V, "-1D")' in src
|
||||
assert not hasattr(C, "spot_causale"), "lo shift locale e' stato rimosso: vive in cblib.causale"
|
||||
# cblib e' causale (controllo positivo sul difetto che ripara)
|
||||
idx = pd.date_range("2026-06-01", periods=3, freq="h", tz="UTC")
|
||||
S = pd.Series([1.0, 2.0, 3.0], index=idx)
|
||||
assert cblib.causale(S, "1h").asof(idx[1]) == 1.0 and S.asof(idx[1]) == 2.0
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
@@ -327,7 +330,8 @@ def test_trade_CONTA_le_ore_tagliate_invece_di_nasconderle():
|
||||
|
||||
def test_f_net_e_il_credito_reale_su_quello_modellato():
|
||||
"""Il credito reale si incassa al BID della corta e si paga all'ASK della lunga: e' la
|
||||
convenzione conservativa di `cblib.f_factors`, e vale 0,74 su ETH nel crollo."""
|
||||
convenzione conservativa di `cblib.f_factors`, e vale 0,76 su ETH nel crollo (0,74 con lo spot
|
||||
e il DVOL non causali della mattina del 09/09: i due numeri vanno cambiati insieme)."""
|
||||
t = _trade(C.SHORT_D, C.LONG_D)
|
||||
assert t["cred_real"] == pytest.approx((0.030 - 0.012) * 2000.0)
|
||||
T = (C.EXP - TS0).total_seconds() / 86400.0 / 365.25
|
||||
@@ -477,7 +481,7 @@ def test_integrazione_la_misura_del_09_09_si_riproduce():
|
||||
|
||||
eth = s["ETH"]
|
||||
assert eth["n_pre"] > 200, "il campione pre-crollo si e' svuotato"
|
||||
assert eth["f_net"] == pytest.approx(0.74, abs=0.03)
|
||||
assert eth["f_net"] == pytest.approx(0.76, abs=0.03)
|
||||
assert eth["f_exp"] == pytest.approx(1.04, abs=0.03)
|
||||
assert eth["q_itm_pieno"] == pytest.approx(0.83, abs=0.10), (
|
||||
"la quota di trade a f_exp tautologico e' cambiata: cambia la lettura del 1,04")
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Revisione settimanale (src/live/revisione.py): sola lettura, firmata, e il silenzio non e' un esito.
|
||||
|
||||
Nessun test chiama il modello o Telegram: `interroga` e `invia` sono iniettati. Nessun test scrive
|
||||
nel repo: tutto in una radice temporanea con file sintetici.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
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 revisione as R # noqa: E402
|
||||
from src.live.venue_probe import in_release_window # noqa: E402
|
||||
|
||||
ADESSO = datetime(2026, 9, 14, 6, 15, tzinfo=timezone.utc) # un lunedi'
|
||||
|
||||
|
||||
def _radice(tmp_path: Path) -> Path:
|
||||
(tmp_path / "CLAUDE.md").write_text("# CLAUDE\n\n## 4. Gate\n| STATARB | 2026-09-27 | soglie | ritiro |\n")
|
||||
(tmp_path / "config").mkdir(); (tmp_path / "config" / "live.json").write_text('{"disaster_sl_pct": 0.30}')
|
||||
j = tmp_path / "docs" / "journal"; j.mkdir(parents=True)
|
||||
for i in range(1, 10):
|
||||
g = (ADESSO - timedelta(days=i)).date().isoformat()
|
||||
(j / f"{g}.md").write_text(f"# Giornale {g}\nEquity $4,477.46 · leva 0.26x\n")
|
||||
d = tmp_path / "docs" / "diary"; d.mkdir()
|
||||
(d / "2026-09-09b-recente.md").write_text("recente: f 0,712\n")
|
||||
(d / "2026-08-20-vecchio.md").write_text("vecchio\n")
|
||||
(d / "senza-data.md").write_text("senza data\n")
|
||||
live = tmp_path / "data" / "live"; live.mkdir(parents=True)
|
||||
(live / "scale_watch_state.json").write_text('{"scala": 1.0}')
|
||||
(live / "usde_watch.jsonl").write_text("\n".join(f'{{"i": {i}}}' for i in range(10)) + "\n")
|
||||
return tmp_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- materiale
|
||||
|
||||
def test_il_materiale_prende_la_finestra_dichiarata_e_dice_cosa_non_legge(tmp_path):
|
||||
voci = R.materiale(_radice(tmp_path), ADESSO)
|
||||
nomi = [n for n, _, _ in voci]
|
||||
giornali = [n for n in nomi if n.startswith("giornale ")]
|
||||
assert len(giornali) == R.FINESTRA_GIORNALE_G, "7 pagine, non 9: la finestra e' dichiarata"
|
||||
assert "giornale 2026-09-13" in nomi and "giornale 2026-09-07" in nomi and "giornale 2026-09-06" not in nomi
|
||||
assert "diario 2026-09-09b-recente.md" in nomi
|
||||
assert "diario 2026-08-20-vecchio.md" not in nomi and "diario senza-data.md" not in nomi
|
||||
testo = dict((n, t) for n, t, _ in voci)
|
||||
assert "0,712" in testo["diario 2026-09-09b-recente.md"]
|
||||
assert testo["stato usde_watch.jsonl (ultime 3)"].splitlines() == ['{"i": 7}', '{"i": 8}', '{"i": 9}']
|
||||
assert testo["stato scale_history.jsonl (ultime 3)"] == "(assente)", "un file assente si DICE (P5)"
|
||||
assert testo["git log dal 2026-08-31 + stato del working tree"].startswith("non leggibile"), \
|
||||
"in una cartella che non e' un repo il log non si inventa"
|
||||
assert any(n.startswith("monitor_health") for n in nomi)
|
||||
|
||||
|
||||
def test_i_sorveglianti_vengono_prima_delle_voci_grandi(tmp_path):
|
||||
"""Il tetto totale taglia in coda: gli stati (piccoli) devono stare PRIMA di giornale e diari,
|
||||
altrimenti sono i primi a sparire — e' successo alla prima stesura (usde_convert a 2 caratteri)."""
|
||||
nomi = [n for n, _, _ in R.materiale(_radice(tmp_path), ADESSO)]
|
||||
i_stato = max(i for i, n in enumerate(nomi) if n.startswith("stato "))
|
||||
i_giornale = min(i for i, n in enumerate(nomi) if n.startswith("giornale "))
|
||||
assert i_stato < i_giornale
|
||||
|
||||
|
||||
def test_il_troncamento_e_dichiarato(tmp_path, monkeypatch):
|
||||
r = _radice(tmp_path)
|
||||
(r / "CLAUDE.md").write_text("x" * 1000)
|
||||
monkeypatch.setattr(R, "MAX_CHARS_VOCE", 100)
|
||||
n, t, tr = R.materiale(r, ADESSO)[0]
|
||||
assert tr and "[… troncato a 100 caratteri su 1,000]" in t
|
||||
assert "[TRONCATO]" in R.costruisci_prompt([(n, t, tr)], ADESSO)
|
||||
|
||||
|
||||
def test_il_prompt_porta_le_regole_e_la_data(tmp_path):
|
||||
p = R.costruisci_prompt(R.materiale(_radice(tmp_path), ADESSO), ADESSO)
|
||||
assert "DATA DELLA REVISIONE: 2026-09-14 06:15Z" in p
|
||||
for t in R.TITOLI:
|
||||
assert t in p
|
||||
for parola in ("SOLA LETTURA", "CITA LA FONTE", "DECISIONI VINCOLANTI", "docs/memory", "I GATE HANNO UNA DATA",
|
||||
"NIENTE PREVISIONI"):
|
||||
assert parola in p, parola
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- guardia
|
||||
|
||||
def _revisione(sintesi="Tutto letto. Equity $4,477.46.", extra=""):
|
||||
return ("## Sintesi\n" + sintesi + "\n## Cosa non torna\nniente\n## Cosa e' maturo da decidere\n"
|
||||
"STATARB il 2026-09-27\n## Proposte (nessuna azione eseguita)\nnessuna\n"
|
||||
"## Cosa ho letto e cosa mi mancava\nCLAUDE.md" + extra)
|
||||
|
||||
|
||||
def test_la_guardia_rifiuta_il_vuoto_e_i_titoli_mancanti():
|
||||
assert R.valida("", "fonte")[0] == "rifiutata"
|
||||
stato, motivi = R.valida("## Sintesi\nsolo questa", "fonte")
|
||||
assert stato == "rifiutata" and "titoli mancanti" in motivi[0]
|
||||
|
||||
|
||||
def test_la_guardia_sui_numeri_ha_il_controllo_positivo():
|
||||
fonte = "Equity $4,477.46 · leva 0.26x · 2026-09-27"
|
||||
assert R.valida(_revisione(), fonte) == ("ok", [])
|
||||
stato, motivi = R.valida(_revisione(sintesi="Equity 9.999,99 e leva 3,14x"), fonte)
|
||||
assert stato == "sospetta" and "non presenti nel materiale" in motivi[0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- rapporto e Telegram
|
||||
|
||||
def test_il_rapporto_e_firmato_e_dichiara_la_sola_lettura():
|
||||
voci = [("CLAUDE.md", "x" * 10, False), ("giornale", "y", True)]
|
||||
r = R.rapporto(_revisione(), None, "ok", [], "claude-fable-5-1", ADESSO, voci)
|
||||
assert r.startswith("# Revisione settimanale — 2026-09-14")
|
||||
for frase in ("`claude-fable-5-1`", "SOLA LETTURA", "lettore fallibile (P13)", "## Firma e materiale",
|
||||
"| CLAUDE.md | 10 | |", "| giornale | 1 | si |", "l'unico file"):
|
||||
assert frase in r, frase
|
||||
|
||||
|
||||
def test_un_modello_muto_produce_un_rapporto_che_lo_dice():
|
||||
r = R.rapporto(None, "timeout dopo 900s", "rifiutata", ["timeout dopo 900s"], "m", ADESSO, [])
|
||||
assert "## Revisione NON eseguita" in r and "timeout dopo 900s" in r and "P5" in r
|
||||
tg = R.per_telegram(None, "timeout dopo 900s", "rifiutata", ADESSO, Path("docs/revisioni/x.md"))
|
||||
assert tg.startswith("<b>🚨") and "timeout" in tg
|
||||
|
||||
|
||||
def test_telegram_porta_la_sintesi_e_dichiara_il_taglio():
|
||||
tg = R.per_telegram(_revisione(sintesi="A <b>corta</b>."), None, "ok", ADESSO, Path("p.md"))
|
||||
assert "A <b>corta</b>." in tg and "niente" not in tg, "solo la Sintesi, con l'HTML neutralizzato"
|
||||
lunga = R.per_telegram(_revisione(sintesi="z" * 5000), None, "ok", ADESSO, Path("p.md"))
|
||||
assert "[… sintesi tagliata" in lunga and len(lunga) < 4096
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- orchestrazione: cosa scrive, cosa no
|
||||
|
||||
def _albero(root: Path) -> set[str]:
|
||||
return {str(p.relative_to(root)) for p in root.rglob("*") if p.is_file()}
|
||||
|
||||
|
||||
def test_secco_non_chiama_e_non_scrive(tmp_path, capsys):
|
||||
r = _radice(tmp_path); prima = _albero(r)
|
||||
|
||||
def mai(*a, **k):
|
||||
raise AssertionError("il modello NON va chiamato in --secco")
|
||||
esito = R.scrivi(r, ADESSO, secco=True, interroga=mai, invia=mai)
|
||||
assert esito["secco"] and _albero(r) == prima
|
||||
assert "SECCO" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_un_giro_riuscito_scrive_UN_solo_file_e_manda_la_sintesi(tmp_path):
|
||||
r = _radice(tmp_path); prima = _albero(r); mandati = []
|
||||
esito = R.scrivi(r, ADESSO, modello="claude-fable-5-1", quiet=True,
|
||||
interroga=lambda p, m, t: (_revisione(), None), invia=lambda h: mandati.append(h) or True)
|
||||
nuovi = _albero(r) - prima
|
||||
assert nuovi == {"docs/revisioni/2026-09-14.md"}, "l'unico file scritto e' il rapporto"
|
||||
assert esito["stato"] == "ok" and esito["telegram"] is True
|
||||
assert "## Sintesi" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||
assert len(mandati) == 1 and "Revisione settimanale" in mandati[0]
|
||||
|
||||
|
||||
def test_un_errore_del_modello_scrive_il_rapporto_e_allerta(tmp_path):
|
||||
r = _radice(tmp_path); mandati = []
|
||||
esito = R.scrivi(r, ADESSO, quiet=True, interroga=lambda p, m, t: ("", "CLI `claude` non trovata"),
|
||||
invia=lambda h: mandati.append(h) or True)
|
||||
assert esito["stato"] == "rifiutata" and "non trovata" in esito["errore"]
|
||||
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||
assert mandati and mandati[0].startswith("<b>🚨")
|
||||
|
||||
|
||||
def test_una_risposta_che_non_e_una_revisione_e_un_errore_dichiarato(tmp_path):
|
||||
r = _radice(tmp_path)
|
||||
esito = R.scrivi(r, ADESSO, quiet=True, telegram=False, interroga=lambda p, m, t: ("ciao", None))
|
||||
assert esito["stato"] == "rifiutata" and "titoli mancanti" in esito["errore"]
|
||||
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||
|
||||
|
||||
def test_no_telegram_non_manda(tmp_path):
|
||||
r = _radice(tmp_path)
|
||||
|
||||
def mai(h):
|
||||
raise AssertionError("Telegram NON va chiamato con telegram=False")
|
||||
esito = R.scrivi(r, ADESSO, quiet=True, telegram=False, interroga=lambda p, m, t: (_revisione(), None), invia=mai)
|
||||
assert "telegram" not in esito
|
||||
|
||||
|
||||
def test_il_prompt_va_su_stdin_non_su_argv(monkeypatch):
|
||||
"""Al primo giro reale (09/09) 195k caratteri su argv hanno dato `Argument list too long` PRIMA
|
||||
di chiamare il modello. DEVE FALLIRE SE: qualcuno rimette il prompt fra gli argomenti."""
|
||||
visto = {}
|
||||
|
||||
class _P:
|
||||
returncode, stdout, stderr = 0, _revisione(), ""
|
||||
|
||||
def finto_run(args, **kw):
|
||||
visto["args"], visto["input"] = args, kw.get("input")
|
||||
return _P()
|
||||
monkeypatch.setattr(R.subprocess, "run", finto_run)
|
||||
prompt = "PROMPT " * 40_000
|
||||
testo, errore = R.interroga(prompt, "claude-fable-5-1", 5)
|
||||
assert errore is None and "## Sintesi" in testo
|
||||
assert visto["input"] == prompt and all(len(a) < 200 for a in visto["args"])
|
||||
assert "--model" in visto["args"] and "claude-fable-5-1" in visto["args"]
|
||||
|
||||
|
||||
def test_una_eccezione_nella_chiamata_diventa_un_errore_non_un_crash(tmp_path, monkeypatch):
|
||||
"""Controllo positivo sul difetto del primo giro: `OSError` dalla CLI → rapporto NON eseguita + 🚨."""
|
||||
def esplode(args, **kw):
|
||||
raise OSError(7, "Argument list too long")
|
||||
monkeypatch.setattr(R.subprocess, "run", esplode)
|
||||
assert R.interroga("x", "m", 5) == ("", "OSError: [Errno 7] Argument list too long")
|
||||
r = _radice(tmp_path); mandati = []
|
||||
|
||||
def esplode_in_scrivi(p, m, t):
|
||||
raise RuntimeError("boom")
|
||||
esito = R.scrivi(r, ADESSO, quiet=True, interroga=esplode_in_scrivi, invia=lambda h: mandati.append(h) or True)
|
||||
assert esito["stato"] == "rifiutata" and "RuntimeError: boom" in esito["errore"]
|
||||
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||
assert mandati and mandati[0].startswith("<b>🚨")
|
||||
|
||||
|
||||
def test_il_revisore_non_e_il_modello_che_scrive_il_codice():
|
||||
from src.live import analista as A
|
||||
assert R.MODELLO == "claude-fable-5-1" and R.MODELLO != A.MODELLO_DEFAULT
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- cadenza: tre dichiarazioni d'accordo
|
||||
|
||||
def _riga_crontab() -> str | None:
|
||||
try:
|
||||
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
righe = [r for r in out.stdout.splitlines() if "cron_review.sh" in r and not r.lstrip().startswith("#")]
|
||||
return righe[0] if righe else ""
|
||||
|
||||
|
||||
def test_la_cadenza_dichiarata_nel_cron_sh_e_quella_installata_e_fuori_dagli_slot():
|
||||
sh = (ROOT / "scripts" / "cron_review.sh").read_text()
|
||||
m = re.search(r"lunedi' (\d\d):(\d\d) UTC", sh)
|
||||
assert m, "cron_review.sh deve dichiarare «lunedi' HH:MM UTC»"
|
||||
ora, minuto = int(m.group(1)), int(m.group(2))
|
||||
assert minuto != 0, "mai al minuto tondo (rate-limit per-IP)"
|
||||
prossimo = ADESSO.replace(hour=ora, minute=minuto) # ADESSO e' un lunedi'
|
||||
assert prossimo.weekday() == 0 and not in_release_window(prossimo)
|
||||
assert "revisione.py --quiet" in sh
|
||||
riga = _riga_crontab()
|
||||
if riga is None:
|
||||
pytest.skip("crontab non leggibile: la dichiarazione installata non e' verificabile qui")
|
||||
assert riga, "crontab leggibile ma cron_review.sh NON installato"
|
||||
campi = riga.split()
|
||||
assert campi[:5] == [str(minuto), str(ora), "*", "*", "1"], f"installata {campi[:5]}, dichiarata {minuto} {ora} * * 1"
|
||||
Reference in New Issue
Block a user