64 Commits

Author SHA1 Message Date
Adriano bebfa63213 tradesdb: fill cron_log arricchiti con ora, prezzo e pos_after dal jsonl (#40)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 17:51:15 +00:00
Adriano ae887a3d98 book_execute: pos_after di nuovo nel jsonl, letta subito dopo il rebalance in un try (#39)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 17:42:34 +00:00
Adriano d34b0a81d8 merge lavoro/goldrake-u11 (secondo giro 27/09) 2026-09-27 15:40:43 +00:00
Adriano cfb0e1e57f merge lavoro/goldrake-u10 (secondo giro 27/09) 2026-09-27 15:40:43 +00:00
Adriano 39adfbf267 merge lavoro/goldrake-u9 (secondo giro 27/09) 2026-09-27 15:40:42 +00:00
Adriano 81b495eb10 merge lavoro/goldrake-u8 (secondo giro 27/09) 2026-09-27 15:40:42 +00:00
Adriano 28a6d79e4f tradesdb: refilo di uno spazio in un commento (#37 C11) 2026-09-27 15:26:43 +00:00
Adriano ac6ad7f116 revisione: tetto per-voce dedicato per CLAUDE.md (150k, il generico restava a 90k con margine minimo) e avviso quando una voce supera il 95% del suo tetto (#37 C12) 2026-09-27 15:24:36 +00:00
Adriano ddcd3773a7 cron_book/cron_cuscino: flock -n sul conto, un giro occupato si salta e si scrive (#36)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:19:49 +00:00
Adriano 213dd12bab docstring: execution/deribit/book allineati al codice attuale, niente importi cablati (#35 A9)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:19:37 +00:00
Adriano 3af6e1f0c3 telegram_daily: gate da monitor_health (non cablati), formato giorni a tre casi, header condizionale su flat (#35 C14)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:18:25 +00:00
Adriano 829ebc7f98 book: min_order_usd da config nel report (#35 A7)
book_report costruiva l'ordine con min_usd=5.0 fisso invece di leggere
min_order_usd da config/live.json, come fa gia' scripts/live/book_execute.py
(default 5.0 se assente). Oggi config = 5, nessun cambio di comportamento.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:17:42 +00:00
Adriano 52a905d497 book: leva_tetto unica per cuscino USDE e giornale (#28)
La leva lorda massima del libro era ridichiarata con tre formule diverse:
usde.frazione_cuscino (N_ASSET fisso, senza scala), la regola `leva` del
giornale (senza scala) e journal.movimenti_capitale (con scala). A scala
1,25 il cuscino USDC risultava 0,30 dell'equity invece di 0,375.

Nuova src/live/book.leva_tetto(cfg) = len(ASSETS) x min(WEIGHT, frac) x
scala, al piu' LEVA_LORDA_MAX; su ScalaNonAutorizzata non solleva, ritorna
il tetto di codice (cuscino_watch non deve morire sulla config sbagliata).
usde.frazione_cuscino e la regola `leva` del giornale ora la derivano;
movimenti_capitale idem, nel suo try/except esistente.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:17:01 +00:00
Adriano 12291709ed revisione, analista: taglio Telegram sul testo grezzo, prima di escape/markup (#35 C13)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:15:22 +00:00
Adriano 8e75e9c9a7 sleeves+edge_watch: pesi derivati da book (import locale), titolo kill corretto (#35 B12)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:15:07 +00:00
Adriano 13c136cf4b tradesdb: docstring corretto (jsonl ha l'ora vera dal 24/08, non piu' la barra); trades_db sync confronta i conteggi jsonl/DB e notifica una volta per episodio (#37 C11, #35 A9) 2026-09-27 15:14:04 +00:00
Adriano e82f2ee163 venue_probe: atteso hardcoded False in diagnose, l'unico chiamante non cade mai nello slot (#35 B11)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:11:29 +00:00
Adriano d80323ff59 analista: testata mostra trading_giorno (non l'equity grezza) e 'nel giorno', non 'oggi' (#35 C8)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:11:20 +00:00
Adriano d59aea7a0a monitor_health: giro orario dei sorveglianti da 3h con marcatore gia' detto (#30)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:11:10 +00:00
Adriano 31ec767ff5 telegram_daily: soglia di accensione generale su k flip, non il minimo fisso (#35 C3)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:10:04 +00:00
Adriano 95cfb396e7 fee_watch: realized_fee_bps sempre calcolata, divergenza da tier notificata una volta per episodio (#37 B9) 2026-09-27 15:06:40 +00:00
Adriano 604fde689e cuscino_watch: riconverti distingue parziale/fallita rileggendo la quota (#35 B10)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 15:03:02 +00:00
Adriano 841d4c78cd book_execute: integrazione U1+U2 — un solo Telegram in solo protezione, stato unknown, set_open_cap diretto (#18, #21, #22, #24)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 14:14:28 +00:00
Adriano 0e6cf17081 merge lavoro/goldrake-u4 (revisione 27/09) 2026-09-27 13:53:10 +00:00
Adriano dab8a88e1c merge lavoro/goldrake-u3 (revisione 27/09) 2026-09-27 13:53:10 +00:00
Adriano ed4f8fd39b merge lavoro/goldrake-u2 (revisione 27/09) 2026-09-27 13:53:10 +00:00
Adriano 8405fe2a7d merge lavoro/goldrake-u1 (revisione 27/09) 2026-09-27 13:53:10 +00:00
Adriano 7234625863 usde_convert: residui cercati sotto USDC e USDE (#26)
Non e' verificato sotto quale valuta Deribit elenchi lo spot USDE_USDC: la
quotazione (USDC) o la base (USDE). cancella_residui interroga entrambe e
unisce per order_id prima di applicare il filtro instrument+label, cosi' un
residuo elencato sotto USDE non si accumula in eterno per non essere mai
letto. Se una delle due letture solleva, solleva come prima (nessun try
aggiunto attorno al giro).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:45:04 +00:00
Adriano 9ce924d97c book_execute: chiama set_open_cap (U1) se il trader lo espone, prima di rebalance_signed
hasattr perche' U1 e U2 si uniscono dopo questo rilascio: DeribitTrader oggi non ha
set_open_cap, e l'hasattr evita che l'unione dei due rami rompa in un senso o nell'altro.
Si toglie l'hasattr al merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:41:07 +00:00
Adriano 7c72308852 live/deribit,execution: open_orders non ingoia gli errori; ensure_disaster_sl -> unknown (#24)
open_orders solleva RuntimeError se 'all' o 'trigger_all' falliscono, invece di ritornare []
(un bracket disaster-SL perso su 'trigger_all' e' lo stesso difetto di una posizione persa su
'all'). ensure_disaster_sl avvolge la lettura di posizione+open_orders in try/except: su
eccezione ritorna {"state": "unknown", "notes": ...} senza cancellare ne' piazzare, distinto
da "naked" (protezione tolta e non rimessa). shadow.shadow_report chiama open_orders gia'
dentro try/except (non toccato).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:40:50 +00:00
Adriano 65a7f0d8fc book_execute: SOLO PROTEZIONE su equity/SKH ignoti; fill mai persi su rebalance parziale (#21, #22, #23)
Prima, con equity reale non leggibile il giro dimensionava comunque un ordine sul paper_cap
($2.000 nominali) e lo mandava; con SKH ignoto il target restava quello (fail-safe solo nel
segno). Ora: se non si sa quanto vale il conto o qual e' il segnale, nessun rebalance_signed —
ma il disaster-SL si controlla LO STESSO per ogni asset (dipende da posizione/mark, non da
equity/segnale). Giro degradato -> exit 2, solo se armato (non in anteprima dry-run).

Fill persi: rebalance_signed puo' eseguire un fill (es. il close di un flip) e poi sollevare nel
passo successivo (l'open dall'altra parte) — quel fill spariva perche' la lettura di `newpos`,
PRIMA del giro che lo registrava, andava in eccezione a sua volta. Estratta la registrazione in
_registra_fill(...), chiamata SUBITO dopo ogni fill (anche quelli di RebalanceParziale, nuova
eccezione di execution.py con .fills/.causa) e PRIMA di leggere la posizione; la lettura ora e'
in un try separato, non fatale (pos_after=None + "reconcile: pos illeggibile" se fallisce).

Aggiornati due test che assumevano il comportamento vecchio (eq_fallback "prosegue" sul paper
capital) e il guard sulla sorgente di test_tradesdb.py (bar_ts ora e' un parametro di
_registra_fill, non piu' `r['last_data']` inline).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:40:15 +00:00
Adriano f79fb54221 live/execution: tetto di apertura per strumento da config, non solo MAX_AMOUNT fisso (#18)
DeribitTrader.set_open_cap(instrument, usd_cap, mark) deriva amount_max = 1,10*usd_cap/mark
dal cap di config del libro; _submit lo usa quando presente, MAX_AMOUNT resta il DEFAULT per
chi non lo chiama (il micro-test). Firma per chi la richiama prima di rebalance_signed (U2):
trader.set_open_cap(inst, r["cap_per_asset"] * r["scala"], mark).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:39:54 +00:00
Adriano 4725839041 usde_watch: notify affidabile (allertati) e quota derivata, non piu' rinviata (#29 B2, #35 B3/B4)
#29 B2: lo stato si salvava PRIMA dell'invio e il ritorno di notify() non si
leggeva — un invio fallito restava "gia' detto" per l'episodio intero. Schema
di cuscino_watch.py: nuove_condizioni()/rec["allertati"] contano una
condizione come "gia' detta" solo se l'invio precedente e' RIUSCITO; un
record vecchio senza `allertati` vale come "tutto allertato" (nessun rispam
retroattivo). Lo stato si scrive una volta sola, a fine giro.

#35 B3/B4: la quota non e' piu' una decisione dell'operatore rinviata a
lunedi' — dal 10/09 (issue #7) e' DERIVATA dal cuscino USDC
(usde._nota_cuscino) — quindi tolti DECISIONE_QUOTA_DAL, quota_da_decidere()
e la notify "la decisione di QUOTA e' dovuta". Il tetto di allerta IDONEO
legge c["quota_max_frac"] invece del 50% cablato. La riga "margine" non dice
piu' "conto SEGREGATO" (il modello e' Cross X:SM dal 31/08): stampa il totale
USDC + USDE scontato del cross, coi numeri che la funzione ha gia'.

Le due modifiche toccano lo stesso blocco (allarmi + STATE write + stampa) e
non si possono separare per hunk senza rischio: un solo commit per il file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:38:46 +00:00
Adriano 5acc31e390 venue_watch: BLIND ha una severity propria, il titolo non chiede piu' il prelievo (#31)
Con un asset BLIND e nessun ALERT, severity restava None e il titolo del
runner cadeva sul default piu' forte ("PRIMO PASSO: prelievo di prova") per
un episodio che e' "non vedo", non "stress misurato". run_once ora assegna
severity="blind" quando c'e' un BLIND senza ALERT (ALERT resta piu' forte, P9);
_titolo distingue il caso con "venue_watch CIECO — referenze non leggibili,
nessun giudizio sul venue". La ripetizione oraria di BLIND non e' toccata.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:38:35 +00:00
Adriano dda5d41f38 fee_watch: save_state solo se l'invio necessario e' riuscito (#29 B8)
save_state girava sempre dopo l'eventuale notify, anche quando l'invio
falliva: il giro dopo diff_vs confrontava lo stato nuovo con se stesso e il
cambiamento (o il livello AZIONE) spariva senza essere mai stato detto. Ora
lo stato si salva solo se non serviva un invio, o se l'invio e' riuscito.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:38:28 +00:00
Adriano f0193f013d usde_convert: cancella i residui GTC delle proprie label, nel ciclo e all'avvio (#26)
Il gateway cerbero-mcp non accetta time_in_force (PlaceOrderReq non ha il
campo): un limit riempito solo in parte restava APERTO per sempre e si
accumulava a ogni giro. cancella_residui() pulisce lo spot USDE_USDC dai
residui con label "usde-quota-*" prima del ciclo; nel ciclo, un ordine con
filled < amt viene cancellato subito dopo, e una cancellazione fallita ferma
il ciclo (mai un secondo ordine con un residuo vivo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:38:20 +00:00
Adriano 0c19c804d1 analista: --secco non scrive piu' nel DB (#34)
scrivi() chiamava J.salva() prima del controllo su --secco: un giro "a secco" scriveva
comunque le sezioni misurate nel giornale. Ora salva() (e scrivi_file, che era gia' dopo
il return) girano solo quando non e' secco; rendi_markdown() tollera l'assenza della riga
in DB (nota/analisi vuote, verificato).

Test nuovo in tests/test_analista.py (non c'e' un test_analista_u4.py: l'analisi indicava
i due file journal/tradesdb, questo fix e' su analista.py e segue la convenzione un
file-di-test-per-modulo del resto della suite).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:38:10 +00:00
Adriano 2bf4f9cceb live/deribit: position_usd legge il segno da direction, non dal solo size (#17)
Il gateway manda size/size_currency in assoluto e il segno a parte in direction:
letta com'era, uno short risultava un long e close()/ensure_disaster_sl decidevano
lato ordine e prezzo dello stop dal segno sbagliato.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:37:36 +00:00
Adriano b8c2bba320 tradesdb: lotti con segno (short) e fill a qty<=0 fuori dal FIFO (#32 C2, C10)
C2: fifo_roundtrips ora apre un lotto SHORT quando un sell non trova lotti long da chiudere
(prima il resto spariva in silenzio), chiude prima il lato opposto e apre l'eccedenza nel
segno del fill — anche a meta' dello stesso fill (inversione long<->short). stato_aperto e
report() portano la direzione del lotto residuo.
C10: un fill "NON VERIFICATO" a qty 0 (place_order error) non entra piu' nel FIFO ne' in
stato_aperto (WHERE qty > 0 in rebuild_roundtrips/stato_aperto); resta nel DB marcato
(verified=0), non e' scomparso.

test_fifo_non_mescola_gli_asset aggiornato: una vendita ETH senza lotti long ora apre uno
short invece di sparire (era il comportamento che C2 corregge). Test nuovi in
tests/test_tradesdb_u4.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:37:20 +00:00
Adriano ac21e1c97d journal: dichiarati nello stesso intervallo si sommano, giro corrente, finestra 7gg, DD netto (#27, #35)
#27: due movimenti dichiarati nello stesso intervallo si sommavano al posto di sovrascriversi
(l'aggancio "dichiarato+rilevato" ora cerca solo fonte=="rilevato").
#35 C9: salute() non conta piu' il giro dell'ora corrente prima che scatti (minuto :47 +
margine, MINUTO_GIRO costante di modulo).
#35 C15: la finestra "ultimi 7 giorni" della concentrazione ora copre 7 giorni di calendario,
non 8 (baseline = ultima lettura al o prima di giorno-7 fine giornata).
#35 C7: il drawdown si calcola sulla serie equity al netto dei movimenti di capitale certi,
cosi' un prelievo non si legge come perdita di trading e un versamento non alza il picco.

Test nuovi in tests/test_journal_u4.py; tests/test_journal.py aggiornato (l'orologio del
test su salute() era reale e non deterministico, ora e' fissato).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:35:08 +00:00
Adriano cfc42653cc shadow: USDE illeggibile -> equity IGNOTA non 0; online richiede tutti i mark mainnet (#20, #25)
Un'eccezione sulla lettura del conto USDE valeva 0 USDE: se il conto ne aveva davvero, l'equity
totale risultava sottostimata senza che nulla lo dicesse. Ora _collaterale_usde ritorna None +
la nota dell'eccezione, e _equity propaga l'ignoto (real_eq None -> eq_fallback, non un numero
falso). shadow_report.online guardava solo il mark di BTC: con ETH in fallback il giro si
considerava comunque online. Ora richiede TUTTI i mark da mainnet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 13:33:48 +00:00
Adriano 1a7f670966 sei sorveglianti registrano i giri e stanno in monitor_health (Fixes #16)
src/live/giri.registra_giro: una riga jsonl {ts ms, campi} dopo una misura completata,
mai un'eccezione. edge_watch, fee_watch, scale_watch, vrp_f_watch, venue_news (cron_daily)
e venue_watch (cron_book, orario) scrivono data/<dir>/runs.jsonl; niente riga senza misura.
monitor_health: sei MonitorSpec (30h i giornalieri, 3h venue_watch), open_labeled=False.
Stessa riparazione di cc01 (#10, #12), generalizzata. Primo giro notturno: NUOVO, non allerta.
Test: incrocio P1 percorso/RUNS, misura -> una riga, non-misura -> nessuna. Suite: 1098 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 08:48:07 +00:00
Adriano e4efdaaa53 analista: niente capitale cablato nelle istruzioni, round-trip = lotto FIFO (Fixes #13)
ISTRUZIONI diceva «capitale ~$635» (equity reale ~$4.5k dal 04/09): valore ridichiarato,
forma di P1, e fuori pagina per la regola 1 delle istruzioni stesse. Ora il capitale e'
l'equity in pagina. Aggiunto che fifo_roundtrips chiude un round-trip per lotto: piu'
round-trip che fill nello stesso giorno e' atteso (l'analista lo segnalava come anomalia).
Test: nessun importo in ISTRUZIONI; FIFO e lotto nominati.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 08:48:07 +00:00
Adriano b46041d11d CLAUDE.md: §1 tetto allerta USDE 0,85 e frase ricucita, garanzia dell'ombra anche per permesso; §4 soglie STATARB verbatim (Fixes #14, Fixes #15)
- §1: «tetto ALLERTA quota 50%» contro config usde.quota_max_frac 0.85 (dal 06/09;
  0.50 resta default di codice in src/live/usde.py). Frase «dopo un bonifico si» troncata
  dal 10/09 (37d1565), ricucita.
- §1 OMBRA: dal 21/09 il token dell'ombra e' a scope di sola lettura (Adriano/TradingBook#19),
  misura del 21/09 riportata; il segreto resta pieno, la chiave read-only la crea l'operatore.
- §4 STATARB: regola del 24/07 copiata verbatim da scripts/research/r0724_statarb_deploy_gate.py
  (esiti, guardie, addendum 25/07 non binding). Nessun verdetto anticipato.
Autorizzato da Adriano il 21/09 dopo la revisione settimanale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 08:48:07 +00:00
Adriano 4b04831352 giornali 17-20/09 (cron_daily 00:30Z) e revisione settimanale 21/09 — solo dati
Commit di soli dati dalla produzione: le pagine le scrive cron_daily e la
revisione cron_review, nessun cron le committa (commit a mano da sempre).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 08:25:38 +00:00
Adriano 8007158eb5 giornale 2026-09-16 (cron_daily 00:30Z)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 05:31:42 +00:00
Adriano 31f5915a41 ombra di TradingBook documentata al §1; cc01_regime_watch registra i giri e sta in monitor_health (Fixes #11, Fixes #12)
- CLAUDE.md §1 OMBRA: cron :44 sulla stessa crontab, legge il conto con la STESSA chiave live di
  cerbero-mcp (verificato per hash), scrive solo nel suo albero (3 giri misurati, nessun file di
  data/ o logs/ di qui cambiato), non invia per assenza di codice e non per permesso. §12 rimanda.
- cc01: ogni giro con entrambi gli asset misurati scrive data/cc01_regime/runs.jsonl (ts ms,
  stato, funding 30g/7g); se non vede, non scrive e lo dice. monitor_health: MonitorSpec
  giornaliera, max_age 30h, percorso incrociato con cc01.RUNS da un test (P1).
- tests/test_cc01_regime_watch.py (4, rossi prima). Suite completa: 1074 passed.
Autorizzato da Adriano il 16/09 dopo la revisione settimanale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 21:10:12 +00:00
Adriano e42ab5369f giornali 10-15/09 e revisioni 14/09 (NON eseguita, limite Fable) e 16/09 (rilancio a mano, esito sospetta)
Solo documenti generati da cron_daily e revisione.py, nessun codice. Su richiesta di Adriano 16/09.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 19:46:36 +00:00
Adriano a6a49d9c65 cc01_regime_watch: ROOT in sys.path, moriva ogni notte dal 03/09 su from src.live.cli (Fixes #10)
Eccezione alla regola «PG non si tocca» autorizzata da Adriano il 16/09, solo questo file.
Verificato: --help exit 0, giro completo QUIET (BTC 30g +9,0%, ETH +9,4%), test_cli_flag 55 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 19:36:33 +00:00
Adriano Dal Pastro a4e942c8bb r0910: risk-off ±2h intorno a FOMC/CPI su TP01 REFUTED 0/3 (ΔSh −0,097, −1,24%/anno; le ore dei dati sono fra le migliori)
Calendario letto dal web (Fed 61 riunioni + BLS 88 release CPI, 2019-2026), TP01 orario dalla stessa
held di sleeves, null location-matched 1000 estrazioni (Δ vero al 4,2° pctl), per anno 2/8.
Registro §78, memoria 20-ondate (D-bis), diario 2026-09-10b.

Fixes #9

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
2026-09-10 14:56:29 +00:00
Adriano Dal Pastro 5b72a4b188 §3: quota USDE 0,64 derivata dal cuscino e' decisione vincolante dell'operatore (10/09)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
2026-09-10 14:19:31 +00:00
Adriano Dal Pastro a2b03aa2b6 USDE allineato a quota 0,64 su ordine dell'operatore (238 USDE venduti, slack +$266 = OK)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
2026-09-10 14:17:43 +00:00
Adriano Dal Pastro 37d1565a1e cuscino_watch: riacquisto automatico di USDE con isteresi (ECCEDENTE), bande in config, escape HTML nel sink di notify
Decisione dell'operatore 10/09 («riportarla a quota in autonomia»). Verificato da revisione fable
(10 segnalazioni, 9 applicate; loop numerico su usde_convert.piano: nessuna sequenza vendita→acquisto).

- quarto stato ECCEDENTE: slack > cuscino_riacquisto_frac (0,40) x cuscino => acquisto di USDE fino
  allo stesso bersaglio della vendita, q* = 1 - 0,30 x (1 + cuscino_margine_frac 0,20) = 0,64.
  Isteresi [0 ; 0,40] x cuscino con bersaglio unico 0,20: per oscillare servono +-8,6% di equity
  USDC (~$380); un bonifico alza lo slack di 0,7 x importo e sopra ~9% dell'equity ricompra da solo.
- le tre frazioni (preavviso/margine/riacquisto) in config/live.json sezione usde, lette da
  usde.bande_cuscino() che verifica l'ordine; quota_target 0,70 TOLTO (slack zero per costruzione).
- notifier.notify: html.escape su titolo e valori — la prima allerta vera (13:53Z) era andata persa
  per un '<' nudo; allerta_errore registrato nel record (P3).
- da ECCEDENTE un fallimento si ritenta ogni 24h (non 4 Telegram/giorno per sempre); bande fuori
  ordine => BLIND con motivo, non un traceback ingoiato dal cron.
- CLAUDE.md §1/§4/§5.17, config _nota_usde/_nota_cuscino, diario. 0,64 e' una DERIVAZIONE
  dell'autore, non la quota decisa il 30/08 (0,70): dichiarato, da confermare.
- test: 1070 (+8 netti).

Fixes #7

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
2026-09-10 14:14:29 +00:00
Adriano Dal Pastro f82f685528 cinque punti della revisione 09/09: usde_watch a 1000 trade + finestra 24h, cuscino_watch (equity USDC, riconverte da solo), PREVDAY-01 kill/veto cablati, SCALA-01 al 2027-02-28, versamento 04/09 dichiarato
Decisi dall'operatore il 2026-09-10, verificati da revisione fable (15 segnalazioni, 12 applicate).

- usde_watch: TRADE_LIMIT 1000 (count max Deribit) e trade_copertura(): lista troncata o ultima
  lettura oltre la finestra di 24h del gateway => somma NON leggibile, reward non attribuito (P12).
  Riga del 07/09 corretta nel log con campo `correzione` (400 USDE erano acquisti, non reward).
- cuscino_watch.py (cron :53, monitor_health): equity USDC contro cuscino derivato da
  usde.cuscino_richiesto_usd (formula spostata in src/live/usde.py, usde_convert la importa);
  OK/PREAVVISO/SCOPERTO/BLIND; sotto zero lancia usde_convert --quota quota_ripristino(0.20)=0.64
  --esegui con guardie (execution_enabled, depeg_warn, 1 tentativo/6h). Primo giro: PREAVVISO, +$26.
- usde_convert: il tetto del venue vale solo in ACQUISTO (bloccava la vendita).
- paper_prevday: GATE PREVDAY-01 cablato (2027-06-21, kill Sharpe giornaliero < -0,50 su >=180 g
  attivi, veto >=80% barre ricostruibili + divergenze non crescenti con soglia materiale).
  Oggi: +0,95 su 81 g, 1942/1943 ricostruibili, kill NON MATURO.
- CLAUDE.md: arming 20/06 (TP01) / 23/06 (BOOK); piano EUR 5.000 chiuso col versamento 04/09
  ($2.414,68 dal balance, dichiarato); SCALA-01 non prima del 2027-02-28 (A2 dal 01/09);
  PREVDAY-01 con data, kill, veto; §5.17 riparato con i limiti dichiarati (ratchet, slack zero a 0,70).
- test: 1062 (+31): test_cuscino_watch (16), test_paper_prevday_gate (8), test_usde_watch (+5).

Fixes #2
Fixes #3
Fixes #4
Fixes #5
Fixes #6

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kqvff47UBGeYfj1QeN4zE
2026-09-10 13:24:28 +00:00
Adriano Dal Pastro 79afe41ec2 giornale 2026-09-09 (cron_daily 00:30Z)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
2026-09-10 04:32:48 +00:00
Adriano Dal Pastro 8f2e6c5c2c revisione settimanale in sola lettura: revisore fable, rapporto firmato + Telegram, lunedi' 06:15 UTC
- src/live/revisione.py: materiale (CLAUDE.md, config, crontab, git, monitor_health, stati dei
  sorveglianti, 7 g di giornale e diari, ~195k caratteri), prompt con dieci regole (sola lettura, fonte
  per ogni segnalazione, solo numeri del materiale, §3 non si ripropone, idee nuove solo citando la
  memoria, gate con data e criterio), guardia sui numeri, rapporto firmato (P13), Telegram = Sintesi
  con taglio dichiarato; modello muto o risposta senza titoli -> rapporto «NON eseguita» + 🚨 (P5).
- scripts/live/revisione.py (guardia cli.valida, --secco/--no-telegram/--quiet/--giorno/--modello),
  scripts/cron_review.sh, crontab `15 6 * * 1`.
- primo giro reale: il prompt su argv moriva con `Argument list too long` senza rapporto ne' allerta ->
  prompt su stdin, ogni eccezione diventa errore dichiarato; `analista.pulisci` toglieva i titoli.
  Secondo giro: docs/revisioni/2026-09-09.md, stato sospetta (cifre in formato italiano), nessun URGENTE.
- scartati con la memoria: autoregolazione dei parametri, generazione automatica di strategie, macro
  da internet (diario 2026-09-09c, memoria 40).
- test: tests/test_revisione.py (18) + 2 in test_cli_flag; suite 1031/1031.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
2026-09-09 22:07:50 +00:00
Adriano Dal Pastro 0a2f780d3c cblib causale: spot +1h e DVOL +1 giorno (debito §5.18); §11 rimisurato 0,714 → 0,712, verdetti invariati
- cblib.causale(s, cadenza): spot_series e dvol_series escono causali (asof = ultima chiusura NOTA);
  il DVOL giornaliero era una seconda serie con lo stesso difetto (fino a 24h avanti), verificato contro
  l'API pubblica a 1h. Regolamento ST alle 08:00 invece delle 09:00.
- §11 riprodotto al millesimo su worktree HEAD (0,714 [0,690-0,779], n=19, flag --al) e rimisurato:
  0,712 [0,664-0,732]; solo spot 0,721, solo DVOL 0,693, DVOL orario di controllo 0,717. Mediana onesta
  BTC 1,45 → 2,12, fee 1,51x/1,87x. r0730: 0,73 → 0,71. §75: ETH pre 0,76, BTC durante 0,89 (orario 0,83).
  skew panel ≤0,005. vrp_f_watch: f canonico 0,732 → 0,706, differenza +0,097 [+0,042, +0,140].
- r0909 senza doppio shift, contesto sul DVOL del giorno; r0822 regime su date di calendario; r0901
  join riallineato (bit-exact). Aperti con costo: DVOL orario (−0,04/+0,06 nel crollo), spot 5m (≤25 min).
- test: +3 causalita' in test_cb_chain_vrp, anti-doppio-shift in test_r0909; suite 1011/1011.
- revisione fable: 16 segnalazioni, tutte applicate (fra cui un «−1,8% in un'ora» che era +3,9%).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
2026-09-09 21:41:23 +00:00
Adriano Dal Pastro bb40e87d13 crolli: il libro perde nel giorno e chiude > 0 in 8/8 finestre dal 2022 (SKH01 short); crollo catturato a vol bassa, XRP diluisce come SOL
Quattro misure (r0909_*), nessun cambio a libro/pesi/config:
- libro nei crolli: −0,48%/g nei 160 giorni ≤ −5%, positivo per finestra dal 2022
  per la gamba short di SKH01 (108% dei guadagni); beta 0,0769 riprodotto (§46);
  il peso di SKH01 resta chiuso dal gate (in-sample).
- crollo catturato 1-5/06/2026 a quote vere: f_net 0,74 = rally; put δ−0,10
  1,92× il modello; a vol bassa e fuori dal gate di VRP01 → §3 non si riapre.
- XRP terza gamba (harness r0822_sol_leg): hold-out −0,169 in 0/24, un anno buono.
- universo Deribit: liquidi solo BTC/ETH/XRP/SOL; XS01 13/19; BTCDVOL future
  non negoziabile; PAXG non misurato.
Revisione fable: due conclusioni smontate (MTM 2,6× era un artefatto di quote;
«8/12 guadagna» era l'ordine delle classi). Debito §5.18: cblib.spot_series
+ asof guarda un'ora avanti (feed 1h etichettato all'apertura).
Test Opus: 92 nuovi, suite 1008/1008; due difetti del verdetto XRP corretti.
Docs: diario, RESULTS §74-77, CLAUDE.md, memoria 20/50, README; journal 07-08/09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zJjHUS7mf4pnGE6pq9RTt
2026-09-09 16:33:54 +00:00
Adriano Dal Pastro 9f55652d99 usde al 68,7%: il tetto del venue del 30-31/08 non c'era piu'; il versamento di prova del 25/08 dichiarato, TWR +7,83%
Su autorizzazione dell'operatore («usa piu' USDE», «cerca % massima raggiungibile»):
- usde_convert al 31,8% (774 USDE), poi sonda a saldo crescente r0906_usde_tetto_sonda.py
  (passi 100->20->4->1, doppio rifiuto prima di scendere, conferma a saldo neutro): da 31,8%
  a 68,7% in 39 ordini @ 1,0005, ZERO rifiuti. Fermata dal cuscino di regolamento (70%,
  derivato da config), non dal venue. «Il 70% non e' raggiungibile ne' ora ne' mai» (31/08)
  e' falsificato: il tetto e' sparito, non scalato. Non spiegato, puo' tornare.
- config: venue_cap_frac -> null (misura in venue_cap_misurato), quota_max_frac 0,50 -> 0,85
  (il valore scelto dall'operatore il 30/08 per questo scenario).
- debito §5.17: nessuno sorveglia il cuscino USDC (slack $58; una perdita del libro lo consuma
  da sola).

Versamento di prova di EUR 25 il 25/08 08:00Z (dichiarato dall'operatore): +3,3% su $647, sotto
la soglia del 10% del rilevatore, contato per 12 giorni come trading.
- data/live/movimenti_dichiarati.jsonl (append-only, nel backup) letto da
  journal.movimenti_dichiarati; movimenti_capitale lo fonde coi rilevati (fonte dichiarato,
  importo dell'operatore, dopo = prima + importo; dichiarato+rilevato se coincide; fuori
  letture o riga rotta -> avvisi). Report e nota di giornale lo dicono.
- trading da arming +87,35 -> +62,45; TWR +11,98% -> +7,83%. CLAUDE.md §2 aggiornato.
- tests: +6 in test_journal (conftest isola il file). Suite 916 verdi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSnordG9FT4q8M4MVm85GQ
2026-09-07 05:49:15 +00:00
Adriano Dal Pastro 95424f0755 docs: il README era fermo a 15 giorni PRIMA del reset — riscritto contro lo stato vero
Ultimo commit del README: 2026-06-04. Reset v2.0.0: 2026-06-19. Per 75 giorni la prima pagina
del repo ha pubblicato la libreria pre-reset (FADE/HONEST/PAIRS/TSMOM/SHAPE, PORT01-06) con
Sharpe 7,84/10,06 e CAGR ~79% come risultati correnti — cioe' esattamente i numeri che §2
elenca sotto "non citare", dalla libreria che il reset aveva dichiarato artefatto. Meta' dei
file citati non esiste piu' (strategies.yml, portfolios.yml, scripts/waste, scripts/portfolios,
src/live/multi_runner.py), e l'esecuzione descritta era su TESTNET, che e' la causa del reset.

- README riscritto (423 -> 152 righe): cosa gira adesso (TP01+SKH01 75/25, ~$2.050, cadenza
  oraria :47), i numeri nella lente di §2 (TWR +10,6%, non la crescita del conto), la riga che
  ordina il piano, il metodo e i suoi sei requisiti, il dato, la struttura VERIFICATA file per
  file, i comandi, i gate con le loro date, l'obiettivo con la sua onesta'.
- CLAUDE.md §0 e memoria 40: registrato il difetto e la lezione — un reset invalida anche i
  documenti che nessuno rilegge; l'inventario di cosa cita numeri morti va fatto il giorno del
  reset, non 75 giorni dopo per caso.
- Diario 02/09c: sezione col confronto riga per riga; coda dichiarata (l'inventario completo
  degli altri documenti pre-reset non e' stato fatto).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:53:42 +00:00
Adriano Dal Pastro de83909db9 un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia
MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync()
e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag
sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py
avrebbe speso una chiamata al modello e mandato un Telegram.

- src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete.
  --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei
  previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script
  che il cron gia' chiama.
- 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati.
- tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima
  istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati
  (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15).
  Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run.

Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None
con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia.

Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader
importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che
lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le
righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron).

Test 910 verdi (+52). Diario 2026-09-02c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:49:40 +00:00
Adriano Dal Pastro 1279ea605a revisione 02/09, seconda tornata: il classificatore dei movimenti era cieco ~23 ore al giorno
Il feed 1h certificato si ferma alle 00:00: per le letture successive `asof` dava la stessa
barra a t0 e t1, mercato "fermo" = 0, e qualunque calo >=10% del giorno sarebbe stato un
"movimento" scorporato come prelievo (giornale dal 25/08, report da oggi). Con feed assente
tutto era "ambiguo" e il report stampava e1/e0-1 sotto l'etichetta TWR.

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

Test +11 (858 verdi).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:33:13 +00:00
Adriano Dal Pastro 835e0c8666 revisione 02/09: il rotolante si CONTROLLA ogni ora (non "si ri-ancora"), TWR con limiti dichiarati e tre stati
Quattro segnalazioni della revisione sui commit di oggi, tutte verificate e riparate.

1. book_execute.py (+ CLAUDE.md §5.7, memoria 40): «il disaster-SL rotolante si ri-ancora ogni
   ora» era falso — ensure_disaster_sl lascia il bracket finche' lo stop e' entro il 5% e la
   taglia entro il 10%; il giro orario CONTROLLA, ri-ancora oltre la tolleranza (mark
   +5,263%/-4,762%). Lo stop siede fra -26,3% e -33,3% dal mark corrente.
2. journal.rendimento_twr: limite dichiarato (D5) — l'intervallo che contiene un movimento
   certo esce intero dal rendimento, il suo P&L di mercato va in `certi`; errore massimo meta'
   del movimento per costruzione (25/08: ~$0,5). Non si stima (P12). Segmenti a lunghezza zero
   non prodotti; nessun tempo a mercato -> 0,0 dichiarato numero.
3. base di equity zero: `twr` E `trading` None con motivo (il salto 0->X e' invisibile al
   classificatore, `trading` valeva l'intero conto); il report stampa n/d.
4. test_book_cadenza: la guardia estrae ogni prescrizione «ogni/every N unita'» e pretende 60
   minuti, con controllo positivo (M15); limite P13 dichiarato.

Test +5 (847 verdi). Diari 02/09 e 02/09b con la sezione "Revisione".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke
2026-09-02 15:17:05 +00:00
Adriano Dal Pastro 861cc7fc27 debito 7: la cadenza del libro e' ORARIA — docstring riscritto, tre fonti tenute d'accordo da un test
Il docstring di book_execute.py prescriveva «ogni ~230 minuti» mentre il cron gira ogni ora
(47 * * * *). Era il docstring a sbagliare: sulla riga 4h BTC raddoppia gli scatti del
disaster-SL rotolante (r0823_sl_anchor.py), e chi avesse "corretto" il cron verso il
docstring avrebbe spostato il libro sulla riga peggiore.

- scripts/live/book_execute.py: CADENZA: ORARIA, la riga di crontab, la ragione (giro
  idempotente, latenza SKH01 <=1h, ri-ancoraggio orario) e il divieto esplicito con la data.
- tests/test_book_cadenza.py (10): deriva e confronta docstring, intestazione di
  cron_book.sh e crontab installata (crontab -l; SALTATO se illeggibile, non verde);
  parser a 5 campi solo per cadenze regolari; 60 < 230 < 240; minuto != :00.
- docs: CLAUDE.md §5.7 chiuso, §13 conteggio 842; memoria 40; diario 02/09b.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke
2026-09-02 14:38:53 +00:00
133 changed files with 13500 additions and 893 deletions
+6
View File
@@ -79,6 +79,12 @@ data/fee_watch/
# battuta di cuore del collettore catena (stato runtime, come gli altri monitor)
data/chain_collect/
data/vrp_f_watch/
# giri misurati del trigger di regime CC01 (letti da monitor_health, #12)
data/cc01_regime/
# giri misurati dei sorveglianti generalizzati da cc01 (#16), letti da monitor_health
data/edge_watch/
data/scale_watch/
data/venue_news/
# log esecuzioni del book live (stato runtime, contiene fill/fee del conto reale)
data/live/
+194 -28
View File
File diff suppressed because one or more lines are too long
+117 -387
View File
@@ -1,423 +1,153 @@
# PythagorasGoal
Sistema di riconoscimento pattern frattali e predizione per il trading di criptovalute (BTC, ETH), ispirato al framework teorico di Serleto & Malanga (*Pythagoras Trading Prediction*).
Ricerca e esecuzione di strategie algoritmiche su BTC/ETH, con **un libro che gira con soldi veri**
su Deribit mainnet dal 20 giugno 2026.
## Obiettivo
> 🚨 **v2.0.0 — RESET del 2026-06-19. Tutto ciò che questo README diceva prima è archiviato in
> `Old/` e non è fidato.** L'intera libreria di strategie "validata out-of-sample" (le famiglie
> FADE/HONEST/PAIRS/TSMOM/SHAPE, i portafogli PORT01-06, gli Sharpe fra 6 e 10) era un **artefatto
> di uno storico contaminato**: print fantasma di un feed *testnet* più storico Binance/USDT.
> Ri-testate sul feed reale ricostruito da Deribit mainnet, **perdono ogni anno**. Documento di
> fondazione: `docs/diary/2026-06-19-deribit-history.md`.
>
> Questo README descrive il progetto **dopo** il reset. È stato riscritto il 2026-09-02, dopo
> essere rimasto fermo al 4 giugno — quindici giorni prima del reset — mentre pubblicava i numeri
> che il progetto aveva già dichiarato falsi.
Partendo da un capitale iniziale di €1.000, raggiungere un profitto medio di €50 al giorno entro 6–8 mesi, tramite un portafoglio di strategie algoritmiche poco correlate fra loro — mean-reversion, trend/rotazione e spread market-neutral — validate out-of-sample e fee-aware.
**L'autorità sui numeri e sulle decisioni è `CLAUDE.md`**, e il racconto completo sta in
`docs/memory/`. Questo file è l'ingresso, non la fonte.
## Risultati
---
> ⚠️ **Revisione 2026-05-28.** La famiglia squeeze-breakout (SQ/MT/ML/AD/CM/PD, con
> accuracy storiche dichiarate 76-82%) è stata **scartata**: quei numeri erano un
> **artefatto di look-ahead**. I backtest decidevano la direzione dalla candela di
> breakout `close[i]` ma entravano a `close[i-1]` — impossibile dal vivo. Sotto
> ingresso onesto (`close[i]`) e fee reali, l'edge sparisce e tutte perdono, anche
> a fee zero. Dettagli e prove: `scripts/analysis/oos_validation.py`.
## Cos'è vivo, adesso
Dopo una validazione **out-of-sample, fee-aware** di molte famiglie di strategie,
emergono cinque famiglie con edge netto reale, tutte radicate nella stessa lezione
(in cripto la **mean-reversion** funziona, la continuazione no) o nella diversificazione:
| | |
|---|---|
| libro live | **TP01 + SKH01 a 75/25**, nettati in software su una sola posizione per asset (50/50 BTC/ETH) |
| venue | Deribit mainnet, perpetual lineari USDC. Esecuzione **armata** dal 2026-06-20 |
| capitale | ~$2.050 (USDC + USDE), non i €2.000 nominali di nessun paper trader |
| cadenza | **oraria**, minuto `:47` (`scripts/cron_book.sh`) |
| leva | lorda ~0,28x su un tetto di 1,00x. Il cap non ha mai morso: **il vincolo è il segnale** |
| protezione on-book | un solo disaster-SL rotolante al −30% sulla posizione netta |
| Famiglia | Meccanismo | Strategie | Profilo (netto OOS) |
|----------|-----------|-----------|---------------------|
| **FADE** | mean-reversion intraday 1h (long/short, BTC/ETH) | MR01 Bollinger, MR02 Donchian, MR07 Return-reversal | Acc 52-55%, DD 18-34% |
| **HONEST** | long-only multi-regime multi-crypto | DIP01 dip-buy, TR01 EMA-trend, ROT02 dual-momentum | CAGR 31-56%, DD 15-27% |
| **PAIRS** | spread reversion *market-neutral* (2 gambe) | PR01 ETH/BTC, LTC/ETH, ADA/ETH, BTC/LTC, ETH/SOL | Sharpe 2.0-4.4, corr col mercato ~0.05 |
| **TSMOM** | time-series momentum multi-orizzonte | TSM01 (3/6/12m + risk-off) | diversificatore, DD 15-22% |
| **SHAPE** | ML walk-forward su feature di *forma* del prezzo | SH01 (LogisticRegression, orizzonte 12 barre) | diversificatore, corr +0.08 col resto |
**Non** sono nel libro live: XS01, VRP01, GTAA01, XSR01. Vivono nel portafoglio di **ricerca**
(paper, 5 sleeve), che è una serie diversa da quella che gira — quasi tutti i numeri di portafoglio
del progetto sono su quella, non su questa.
Tutti i numeri sono **netti** dopo fee realistiche (Deribit 0.10% RT single-leg, 0.20%
RT/coppia sui pairs), leva 3x, su finestra held-out. Le strategie sono robuste su griglia
parametri, sweep fee 0.00-0.20% RT e — per i pairs — validate con **walk-forward** e
config universale (niente cherry-picking).
## I numeri, con la loro lente
### Portafoglio combinato (la vera leva anti-drawdown)
Ogni numero va scritto con la lente e la banda che gli appartengono. La tabella completa
("non citare" ↔ "citare") è in `CLAUDE.md` §2; qui i tre che contano.
Le famiglie sono **quasi scorrelate fra loro** (~0.05). Combinandole in un unico
portafoglio equipesato il drawdown crolla sotto quello di ogni singola sleeve:
| grandezza | valore onesto |
|---|---|
| rendimento del libro live | **TWR +10,6%** dall'armamento, spezzato sul versamento del 25/08: +11,61% prima, −0,9% dopo |
| crescita di trading | **+$50** in 71 giorni, su 30 round-trip chiusi e $0,87 di fee totali |
| Sharpe del portafoglio di ricerca | **1,95** [1,81 – 2,12] full, **1,54** [1,11 – 1,91] hold-out |
| Portafoglio | CAGR | Max DD | Sharpe |
|-------------|------|--------|--------|
| FADE (6 sleeve) | ~46% | 8% | 3.9 |
| HONEST (3 sleeve) | ~46% | 13% | 2.2 |
| **MASTER** (FADE + HONEST, 9) | ~47% | **5%** | 4.2 |
| **MASTER + PAIRS + TSM01** (15) | ~67% | ~5% | ~6 |
| **PORT06 live** (17 sleeve, cap pairs 33%, leva 2×, config EXIT-16) | ~79% | **2.6%** | 7.8 (FULL) / 10.1 (OOS) |
⚠️ Il rendimento dell'equity **non** è la performance: il 96% della crescita del conto è un
bonifico. Chi legge una percentuale su questo progetto deve sapere se è un TWR o un rapporto fra
due saldi — è stato un difetto reale, riparato il 2026-09-02.
> 🔎 **Numeri sobri (anti-overfit).** L'OOS singolo cade nel regime favorevole 2024-25:
> i valori di Sharpe/DD sopra sono ottimistici di circa il 50%. Da pianificare per le
> decisioni: **Sharpe atteso ~5**, **worst-drawdown su 90 giorni ~6%**, profilo che regge
> a leva 2x con slippage raddoppiato. Configurazione raccomandata: equal-weight, leva 2x,
> con un cap sull'allocazione ai pairs (~30-35%, poiché concentrano ~57% del rischio).
> Tutto resta da confermare nel paper trading live.
## La riga che ordina tutto il resto
## Come funziona
> La ricerca ha smesso di essere il vincolo il 2026-07-26, e **sei ondate successive lo hanno
> confermato invece che ribaltarlo**. I vincoli sono **il capitale che entra** e **il conto che non
> sparisce**.
### MR01 — Bollinger Fade (mean-reversion)
Misurato: il miglior candidato nuovo vale **+0,046 €/giorno**; versare €500/mese invece di €250
porta la probabilità di arrivare al traguardo in 20 anni **dal 14% all'85%**. E €100/mese in più
equivalgono a **+4,07%/anno di drift**, cioè più di tutta la leva autorizzabile.
La strategia attiva sfrutta il fatto, emerso dai dati, che su BTC/ETH a 1h gli estremi
di prezzo **rientrano verso la media** più di quanto proseguano:
## Metodo — cosa deve superare una strategia nuova
1. **Bollinger Bands** (window `n`, `k` deviazioni standard) sul close.
2. **Entry** — quando il close esce *sotto* la banda inferiore → **long** (o *sopra* la superiore → **short**). Ingresso a `close[i]`, eseguibile dal vivo.
3. **Take-profit** alla media mobile (il rientro atteso).
4. **Stop-loss** a `sl_atr × ATR` oltre l'estremo; **time-limit** a `max_bars`.
Sei requisiti, nessuno negoziabile (`CLAUDE.md` §8, gate in `scripts/research/alt/altlib.py`):
Nessun look-ahead: direzione e livelli sono calcolati con dati fino a `close[i]`.
1. **ingresso eseguibile** — direzione e prezzo da dati fino a `close[i]`, mai l'estremo di una candela;
2. **backtest netto** dopo fee Deribit realistiche, più la leva;
3. **out-of-sample** held-out, robustezza su griglia, sweep fee;
4. **liquidità e plausibilità** — un edge su un book fermo o su wick fantasma non è un edge;
5. i **gate**: `marginal_vs_tp01` (Sharpe marginale, non assoluto), `study_family_honest` con
deflated-Sharpe ≥ 0,95, `day_boundary_robust`, `anchor_luck_band`, `weights_tilt_null`;
6. codice, test e diario.
### Le altre famiglie
Il progetto ha **73 regole di prim'ordine** (`CLAUDE.md` §6) — sul dato, sul metodo, sui costi,
sulla produzione e sul piano — ognuna pagata almeno una volta. La più
ricorrente, cinque occorrenze: *un sorvegliante deve derivare il proprio bersaglio dal codice
sorvegliato, mai ridichiararlo — un controllo puntato su una configurazione diversa da quella che
gira passa sempre, e non sta controllando niente.*
- **FADE** (oltre MR01): MR02 fada la rottura del canale Donchian verso il centro;
MR07 fada il movimento di barra estremo misurato in deviazioni standard dei
rendimenti. Stessa logica di reversione, indicatori indipendenti.
- **HONEST** (long-only, multi-crypto): DIP01 compra i dip estremi e rivende al
recupero; TR01 segue il trend con incrocio di EMA su un paniere; ROT02 ruota ogni
giorno sui tre asset col momentum più forte, andando in cash quando BTC è sotto la
sua media (risk-off). Coprono i regimi di trend e rotazione, complementari alle fade.
- **PAIRS** (market-neutral): scommette sul rientro verso la media del log-ratio fra
due cripto (z-score). Long su una, short sull'altra: l'esposizione netta al mercato è
quasi nulla (correlazione ~0.02), il che la rende un diversificatore eccellente.
- **TSMOM**: tiene gli asset con momentum positivo persistente su più orizzonti
(3/6/12 mesi), con overlay risk-off. Rende meno ma è poco correlato, utile in ensemble.
## Il dato
### Perché lo squeeze breakout è stato abbandonato
- **La verità è Deribit mainnet**, perché è dove si esegue. Binance è un audit indipendente, mai
un'ancora per "ripulire": è USDT, ~10 bps fuori, fino al 3% sotto depeg.
- Universo certificato: **solo BTC/ETH**, ogni timeframe. Gli alt sono esclusi.
- Lo storico si aggiorna **solo** con `rebuild_history.py` e si certifica **sempre** con
`certify_feed.py`. Il vecchio downloader è la causa del reset.
- La catena opzioni si **raccoglie** ogni ora (`collect_chain.py`, minuto `:25`): un'ora non
raccolta è persa per sempre.
L'ipotesi originale era opposta — *continuazione* dopo la compressione di volatilità
(Bollinger dentro Keltner → breakout direzionale). Su dati storici sembrava dare
76-82% di accuracy, ma era un **artefatto di look-ahead**: il backtest entrava a
`close[i-1]` con direzione decisa da `close[i]`. Replicando l'esecuzione reale
(ingresso a `close[i]`) l'edge collassa al ~47% (lancio di moneta) e i costi fanno
il resto. Il test sui breakout intra-barra a 5m conferma che il movimento *rientra*
subito (mean-reversion), giustificando MR01. Tutta la famiglia squeeze è in `scripts/waste/`.
### Lezione metodologica
Ogni nuova strategia deve passare: (1) **ingresso eseguibile** senza look-ahead,
(2) backtest **netto** dopo fee realistiche (0.10% RT Deribit), (3) validazione
**out-of-sample** + robustezza su griglia parametri + sweep fee. Strumenti in
`scripts/analysis/` (`strategy_research.py`, `oos_validation.py`, `intrabar_test.py`).
## Struttura progetto
## Struttura
```
PythagorasGoal/
├── src/
│ ├── data/ # Download e gestione dati (Cerbero MCP + Binance)
│ ├── fractal/ # Indicatori frattali: Hurst, Higuchi FD, self-similarity
│ ├── backtest/ # Motore di backtesting con fee e metriche
│ ├── strategies/ # Classe base Strategy ABC + indicatori condivisi
│ │ ├── base.py # Strategy, Signal, BacktestResult, YearlyStats
│ │ └── indicators.py # keltner_ratio, detect_squeezes, ema, atr, rv, corr
│ ├── live/ # Paper trading live su Deribit testnet
│ │ ├── multi_runner.py # Orchestratore multi-strategia (strategie + pairs)
│ │ ├── strategy_worker.py # Worker single-leg con stato persistente
│ │ ├── pairs_worker.py # Worker a 2 gambe per i pairs (market-neutral)
│ │ ├── strategy_loader.py # Import dinamico classi Strategy
│ │ ├── cerbero_client.py # Client HTTP per Cerbero MCP
│ │ ├── signal_engine.py # Squeeze + ML real-time (legacy) + validazione OOS
│ │ └── telegram_notifier.py
│ └── portfolio/ # Portafogli di prima classe (capitale condiviso, backtest + live)
│ ├── base.py # SleeveSpec, Portfolio (.backtest), load_active_portfolio
│ ├── weighting.py # Schemi di ponderazione: equal, cap, inverse_vol, cluster_rp, manual
│ ├── sleeves.py # Builder unificato equity-per-sleeve (fonte unica, parità report)
│ ├── ledger.py # PortfolioLedger: PnL/DD aggregati, persistenza e resume
│ └── runner.py # PortfolioRunner live (Cerbero v2, sizing, ribilancio giornaliero)
├── scripts/
│ ├── strategies/ # Strategie con edge validato OOS (FADE, HONEST, PAIRS, TSMOM + portafogli)
│ ├── portfolios/ # Definizioni PORT01-06 e report run() dei portafogli di prima classe
│ ├── waste/ # Strategie scartate (squeeze SQ/MT/ML/AD/CM/PD, MR03, ROT01, W01-W28)
│ └── analysis/ # Ricerca/validazione OOS fee-aware, gestione rischio, report
├── strategies.yml # Config multi-strategy paper trader
├── data/
│ ├── raw/ # Parquet OHLCV (gitignored, ~70 MB)
│ └── regime/ # DVOL + funding (Deribit mainnet) + cache feature regime (gitignored)
├── VERSION # versione semver (cotta nell'immagine, mostrata nei msg Telegram)
├── docs/
│ ├── diary/ # Diario di ricerca giornaliero
│ └── specs/ # Specifiche di design
├── Dockerfile
├── docker-compose.yml
└── pyproject.toml
src/
data/downloader.py load_data(asset, tf) sui parquet certificati
strategies/ trend_portfolio.py (TP01) · skyhook.py (SKH01) · base · indicators
portfolio/ portfolio.py (N sleeve + weights_tilt_null) · sleeves.py · gtaa.py
backtest/harness.py backtest onesto, senza look-ahead
live/ book.py (esecutore netto) · deribit · livefeed · usde
venue_watch · venue_probe · venue_news · monitor_health · scale_watch
tradesdb · journal · analista · notifier · cli
scripts/
live/ book_execute · trades_db · journal · analista · balance_watch
paper_* (forward-monitor) · usde_watch · usde_convert · fee_watch
research/ r<data>_*.py — un file per esperimento, harness in alt/altlib.py
analysis/ rebuild_history · certify_feed · audit_feed · multi_source_check
cron_{book,daily,chain,balance,usde,opt_snapshot,vol_term}.sh
docs/
memory/ LA MEMORIA — 6 file, indicizzati in testa a CLAUDE.md
research/ RESULTS-0822 (§1-77, un registro per filone) · BRIEF-0822 · SPEC-scale-key
diary/ una voce per esperimento (144)
journal/ libro di bordo, una voce al giorno, 4 livelli per provenienza
tests/ 1008, tutti verdi
Old/ archivio pre-reset — consultabile, non fidato
```
## Strategie attive
Le strategie single-asset estendono `src.strategies.base.Strategy`
(`generate_signals() → backtest()`); i pairs hanno un worker dedicato a 2 gambe.
| Codice | Script | Famiglia | Descrizione |
|--------|--------|----------|-------------|
| **MR01** | `MR01_bollinger_fade.py` | FADE | Fada la banda di Bollinger, TP alla media, SL ad ATR |
| **MR02** | `MR02_donchian_fade.py` | FADE | Fada la rottura del canale Donchian, TP al centro |
| **MR07** | `MR07_return_reversal.py` | FADE | Fada il movimento di barra estremo (z dei rendimenti) |
| **DIP01** | `DIP01_dip_reversion.py` | HONEST | Dip-buy long-only su z-score estremo |
| **TR01** | `TR01_ema_trend.py` | HONEST | EMA 20/100 trend-following su paniere cripto (4h) |
| **ROT02** | `ROT02_dual_momentum.py` | HONEST | Rotazione cross-sectional top-3 + risk-off (1d) |
| **PR01** | `PR01_pairs_reversion.py` | PAIRS | Spread reversion market-neutral su 5 coppie |
| **TSM01** | `tsmom_research.py` | TSMOM | Time-series momentum multi-orizzonte + risk-off |
| **SH01** | `SH01_shape_ml.py` | SHAPE | LogisticRegression walk-forward su 17 feature di forma, orizzonte 12 barre (diversificatore) |
Le fade applicano tre protezioni live: un **filtro trend** (`trend_max`/`ema_long`,
salta i segnali col prezzo troppo esteso rispetto alla EMA200), un **loss-guard Hurst**
(`hurst_max=0.55`, salta i segnali in regime persistente/trending dove si concentrano gli stop-loss
— dimezza il drawdown del portafoglio, calcolato dalle sole close) e l'**EXIT-16 close-confirm SL**
(`sl_confirm_atr=0.5`, 2026-06-04: lo stop scatta solo se la barra *chiude* oltre `sl ∓ 0.5·ATR14` —
gli stop intrabar da wick erano falsi negativi, l'overshoot che buca lo stop è proprio il movimento
che la fade fada; a livello PORT06 porta l'OOS Sharpe da 8.82 a 10.06). Più un filtro `min_tp_frac`
che scarta i micro-scalp col take-profit entro il costo delle fee. Le tre protezioni sono
complementari: Hurst toglie il regime tossico, il trend-filter gli ingressi sovra-estesi, il
close-confirm i falsi stop. Portafogli pronti: `PORT01`
(honest), `PORT02` (fade), `PORT03` (master fade+honest), **`PORT06`** (master esteso, default live).
**Scartate** (in `scripts/waste/`): la famiglia squeeze (SQ01-04, ML01, MT01, PD01,
CM01, AD01 — artefatto di look-ahead), MR03 Keltner (debole/ridondante con MR01) e
ROT01 (dominata da ROT02).
### Comandi utili
## Comandi
```bash
# Backtest di una strategia
uv run python scripts/strategies/MR01_bollinger_fade.py
uv run python scripts/strategies/PR01_pairs_reversion.py
# Ricerca e validazione fee-aware out-of-sample
uv run python scripts/analysis/strategy_research.py # screening famiglie + deep-dive fade
uv run python scripts/analysis/strategy_research_v2.py # MR02 / MR03 / MR07
uv run python scripts/analysis/oos_validation.py # perche' la famiglia squeeze e' scartata
uv run python scripts/analysis/pairs_research.py # ricerca + verifica no-look-ahead dei pairs
# Gestione rischio, combinazione, report
uv run python scripts/analysis/risk_management.py # filtro trend + portafoglio fade
uv run python scripts/analysis/combine_portfolio.py # combinare fade + honest
uv run python scripts/analysis/combine_v2.py # master esteso con pairs + TSM01
uv run python scripts/analysis/report_families.py # report per anno di tutte le famiglie
# Validazione dei worker live (replay == backtest)
uv run python scripts/analysis/validate_worker_mr01.py # worker single-leg su MR01
uv run python scripts/analysis/validate_worker_pairs.py # worker a 2 gambe sui pairs
uv run python scripts/analysis/live_smoke_pairs.py # smoke test feed live reale dei pairs
uv sync # dipendenze
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # storico da Deribit mainnet
uv run python scripts/analysis/certify_feed.py # certifica i feed
uv run python scripts/live/trades_db.py --report # stato del libro + TWR
uv run python scripts/live/journal.py # voce del giorno
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
uv run python scripts/portfolio/run_portfolio.py # portafoglio di ricerca
uv run pytest # 1008 test
```
## Paper Trading Live
Ogni script di `scripts/live/` accetta `--help` e **rifiuta un flag che non conosce** (esce 2): fino
al 2026-09-02 un flag sbagliato eseguiva l'azione di default, e su tre script quell'azione scriveva.
Il multi-strategy runner esegue N strategie in parallelo su dati live da Cerbero MCP,
ognuna con €1000 USDC virtuali indipendenti. Gestisce due tipi di worker:
## Gate aperti
- **Single-leg** (`strategy_worker.py`): per le strategie direzionali. Se un `Signal`
porta `tp`/`sl`/`max_bars` in `metadata` (come le fade), chiude su take-profit /
stop-loss / time-limit; altrimenti usa il fallback `hold_bars`/stop -2%.
- **Due gambe** (`pairs_worker.py`): per i pairs market-neutral. Apre long su una gamba
e short sull'altra, esce sul rientro dello z-score o per time-limit, conta le fee su
entrambe le gambe. Validato: il replay storico coincide *esattamente* col backtest.
Un gate si decide **alla data**, coi criteri scritti **prima**. Elenco completo in `CLAUDE.md` §4.
### Avvio
| gate | data | dove punta oggi |
|---|---|---|
| STATARB | 2026-09-27 | ritiro (Sharpe −1,61 sulla serie vera) |
| SCALA-01 — leva 1,00 → 1,25 | non prima del 2026-10-01 | chiave costruita e **inerte**; 9 condizioni, 2 fatte |
| XSR01 | 2026-10-23 | sotto la lente rendita il meccanismo vale ~0 |
| DVOLSPREAD | kill 2026-10-24 | serie −4,41 |
| GTAA01 tenere/bloccare | al book a $15k | contributo reale +0,10/+0,12 di Sharpe, non incassabile sotto la soglia |
```bash
# Locale
uv run python -m src.live.multi_runner
## Obiettivo, e cosa lo rende difficile
# Docker
docker compose up -d
```
Il target dichiarato è **€50/giorno partendo da €1.000**. Non è raggiungibile a questo capitale:
servono ~$313k (banda [$187k – $1,14M]) o **€1.733/mese per dieci anni** per averlo al 90% di
probabilità. La leva non è la scorciatoia — quella difendibile vale quanto €100-150/mese di
versamento. La via è target-vol, capitale e tempo.
### Configurazione
Le strategie attive sono definite in `strategies.yml`:
```yaml
defaults:
capital: 1000
position_size: 0.15
leverage: 3
strategies: # strategie single-leg
- name: MR01_bollinger_fade
asset: BTC
tf: 1h
enabled: true
params: { bb_window: 50, k: 2.5, sl_atr: 2.0, max_bars: 24, trend_max: 3.0, ema_long: 200 }
pairs: # strategie a 2 gambe (market-neutral)
- name: PR01_pairs_reversion
a: ETH
b: BTC
tf: 1h
enabled: true
params: { n: 50, z_in: 2.0, z_exit: 0.75, max_bars: 72, jump_max: 0.08 }
```
Per aggiungere una strategia: nuova riga in `strategies.yml` (sezione `strategies` o
`pairs`), poi `docker compose restart`. Lo storico delle strategie esistenti rimane intatto.
### Persistenza
Ogni strategia ha la sua directory in `data/paper_trades/`:
```
data/paper_trades/
MR01_bollinger_fade__BTC__1h/
trades.jsonl # Storico trade append-only
status.json # Stato corrente (resume al restart, include tp/sl/max_bars)
```
Notifiche Telegram per ogni trade (richiede `TELEGRAM_BOT_TOKEN` e `TELEGRAM_CHAT_ID` in `.env`).
## Paper Trading a Portafoglio
Accanto al multi-strategy runner originale — in cui ogni strategia gestisce autonomamente il proprio conto virtuale da €1.000 — il progetto dispone ora di un **paper trader a portafoglio** (`src/portfolio/`) che tratta l'insieme delle strategie come un unico organismo con un capitale condiviso.
### Come funziona
La definizione di un portafoglio (`SleeveSpec` + schema di peso) ha due facce sulla stessa sorgente dati:
- **Backtest** (`.backtest()`): ricostruisce le equity-curve di ogni sleeve tramite il builder unificato in `sleeves.py`, le pondera secondo lo schema scelto e calcola le metriche aggregate (CAGR, Sharpe, max DD). La parità con i report prodotti da `report_families.py` è garantita dalla fonte unica.
- **Live** (`PortfolioRunner`): ogni ora il runner scarica le candele aggiornate via Cerbero v2, calcola i pesi correnti, avvia i worker appropriati per ogni sleeve attiva e registra il PnL aggregato nel ledger (`data/portfolios/{code}/`). Il ledger persiste tra i riavvii.
### Schemi di ponderazione
Il modulo `weighting.py` mette a disposizione cinque schemi: `equal` (default), `cap` (tetto per famiglia — p.es. `pairs: 0.33` per limitare la concentrazione), `inverse_vol` (pesi inversamente proporzionali alla volatilità storica), `cluster_rp` (equal tra cluster naturali poi inverse-vol all'interno del cluster) e `manual` (pesi liberi). Lo schema si specifica in `portfolios.yml` insieme al codice portafoglio e alla leva.
### Portafoglio di default: PORT06
La configurazione raccomandata è **PORT06** (`scripts/portfolios/PORT06_master_shape.py`): portafoglio master esteso che include tutte e sei le famiglie (FADE, HONEST, PAIRS, TSMOM, SHAPE), con schema `cap` che limita i pairs al 33% del capitale per moderare la loro concentrazione di rischio. Backtest canonico (dati al 2026-05-28): Sharpe 6.47 (FULL) / 8.82 (OOS), drawdown massimo 4.10% (FULL) / 1.30% (OOS), leva 2×; **con la config live attuale (EXIT-16 close-confirm): Sharpe 7.84 / 10.06, DD 2.60% / 1.15%**.
### Scope live
Il runner esegue **tutti e 17 gli sleeve** di PORT06: **fade** (MR01, MR02, MR07 × BTC/ETH),
**honest** (DIP01, TR01-basket 4h, ROT02-rotation 1d), **pairs** (PR01, cinque coppie),
**TSMOM** (TSM01 1d) e **shape** (SH01 × BTC/ETH). Worker dedicati: `StrategyWorker` (single-leg, fade/
dip/**shape**), `PairsWorker` (2 gambe), `BasketTrendWorker`, `RotationWorker`, `TsmomWorker`. Il runner
fetcha 1h da Cerbero v2 e resampla a 4h/1d; il pool di capitale, il ribilancio giornaliero e il ledger
sono validati == backtest.
> **SH01 (2026-06-01):** gira come `StrategyWorker` normale (il walk-forward è interno a
> `generate_signals`). Il vecchio `MLWorkerWrapper` usava il `SignalEngine` **squeeze scartato** —
> rimosso. **Loss-guard Hurst (2026-06-02):** le fade saltano i segnali in regime persistente
> (rolling-Hurst ≥ 0.55), dove si concentrano gli stop-loss — dimezza il drawdown del portafoglio
> (FULL 4.1%→2.4%; stop-loss fade −67% in numero, perdite totali −68%). Calcolato dalle sole close,
> attivo live (`hurst_max` nei params). Il report orario su Telegram **monitora lo stop-rate fade
> prima/dopo l'attivazione** e dà il verdetto automatico quando il campione è sufficiente.
### Esecuzione reale (shadow, Deribit testnet)
Sette sleeve single-leg — le **6 fade** (MR01/MR02/MR07 × BTC/ETH) e **DIP01** (dal 2026-06-04) —
eseguono ordini **reali su Deribit testnet** accanto al fill simulato (*shadow*: il sim resta la
verità che guida le decisioni; il reale misura la fattibilità). Punti chiave:
- **Strumenti lineari USDC** (`BTC_USDC`/`ETH_USDC-PERPETUAL`): payoff lineare = matematica del
backtest; fee e PnL in USDC. Quantizzazione `Decimal` di amount (step) e prezzi (tick).
- **Take-profit reale = limit reduce-only AL livello** (v1.0.7): piazzato all'apertura, copre la
sola quota del worker (gli strumenti sono condivisi fra worker e nettati per conto); alla
chiusura il worker cancella il resting, riconcilia i fill dal trade history per `order_id` e
chiude a market solo il residuo. Fix della divergenza misurata: il market-on-poll usciva
+235 bps oltre il livello TP. Fill da resting = fee maker (~0%).
- **Stop-loss close-confirm** (v1.1.0): uscita al close che sfonda il livello → market
reduce-only al poll (nessun ordine stop sul book, per scelta: i trigger Deribit generano un
nuovo order_id allo scatto, non verificabile, e i wick non devono stoppare).
- **Verifica sul trade** (order_id in `get_trade_history`), fee reali dai `trades[]`, ledger
reale parallelo persistito (`real_capital`), eventi `REAL_OPEN`/`REAL_TP_RESTING`/`REAL_CLOSE`
nel log + alert Telegram (`REAL_EXEC_LIVE`, `REAL_OPEN_FAIL`).
- Config in `portfolios.yml` → `overrides.execution {enabled, sleeves, instruments}`.
**Pairs/rotation/TSMOM/shape restano simulati**: i pairs richiedono un executor a 2 gambe
(leg-risk), i multi-asset un rebalance-to-target; roadmap nel diario.
### Versione & deploy
Ogni deploy ha una **versione** (file `VERSION`, semver) che compare nei messaggi Telegram (notifiche
trade + report orario), così correli ogni messaggio al codice che l'ha generato. Il sorgente è **cotto
nell'immagine** → per aggiornare il live serve un **rebuild**, non un semplice restart:
```bash
./scripts/deploy.sh # bump patch (1.0.0 → 1.0.1) + commit + rebuild + ricrea container
./scripts/deploy.sh minor # 1.0.x → 1.1.0
```
Il volume `data/` persiste tra i deploy → i worker fanno RESUME dello stato (capitale, posizioni aperte).
### Avvio del paper trader a portafoglio
```bash
# Backtest del portafoglio di default (PORT06)
uv run python scripts/portfolios/PORT06_master_shape.py
# Paper trading live a portafoglio
uv run python -m src.portfolio.runner
# Report orario su Telegram (stato + stop-rate fade prima/dopo loss-guard) — via cron
uv run python scripts/portfolios/hourly_report.py
# Smoke test del data layer Cerbero v2
uv run python scripts/analysis/smoke_portfolio.py
```
## Setup
```bash
# Clona e installa
git clone <repo-url> && cd PythagorasGoal
uv sync
# Scarica dati storici (~70 MB)
uv run python -m src.data.downloader
# Backtest strategia attiva
uv run python scripts/strategies/MR01_bollinger_fade.py
# Paper trading live
uv run python -m src.live.multi_runner
```
### Requisiti
- Python ≥ 3.11
- [uv](https://docs.astral.sh/uv/) come package manager
- Accesso a Cerbero MCP (`cerbero-mcp.tielogic.xyz`) per dati Deribit live
- Docker (opzionale, per deploy su VPS)
## Dati
| Asset | Timeframe | Copertura |
|-------|-----------|-----------|
| BTC, ETH | 5m / 15m / 1h | 2018-01 → oggi |
| SOL, LTC, ADA, XRP, BNB, DOGE | 15m / 1h | 2019-2022 → oggi (variabile per asset) |
Fonte primaria: perpetual Deribit via Cerbero MCP. Fallback: Binance spot via ccxt.
Formato: Apache Parquet (in `data/raw/`, gitignored).
> **Nota sul naming Deribit (per il feed live).** I major sono perpetui *inverse*
> (`BTC-PERPETUAL`, `ETH-PERPETUAL`); gli altcoin sono perpetui *lineari USDC*
> (`SOL_USDC-PERPETUAL`, `LTC_USDC-PERPETUAL`, …) con storia dal 2022. Attenzione:
> `LTC-PERPETUAL`/`ADA-PERPETUAL` non esistono e `SOL-PERPETUAL` restituisce dati
> errati — per gli altcoin usare sempre la forma `_USDC-PERPETUAL`.
### Discovery & validazione strumenti
`src/data/instruments.py` scopre e **valida** gli strumenti disponibili sugli
exchange implementati — **Deribit** e **Hyperliquid** (esclusi Alpaca/stocks e
**Bybit**, feed testnet inaffidabile). Ogni perpetuo viene testato sui dati
storici realmente raccoglibili: esistenza, congruenza OHLC, contratto non-morto,
liquidità e **congruenza prezzo cross-exchange** (mediana per base-coin, tolleranza
5%) — così feed farlocchi e contratti sbagliati (es. `SOL-PERPETUAL`=9.6) vengono
scartati. Il risultato è `data/instruments_registry.json` (strumenti validi +
timeframe + data d'inizio).
**Solo gli strumenti validati possono essere scaricati**: il downloader ha un gate
(`_download_cerbero_range`) che rifiuta quelli non nel registry. Rigenera con:
```bash
uv run python -m src.data.instruments
```
Simboli Deribit: BTC/ETH = `<COIN>-PERPETUAL` (inverse); altcoin =
`<COIN>_USDC-PERPETUAL` (lineari USDC). Registry attuale (testnet): Deribit 18/106
validi (major liquidi, BTC dal 2018), Hyperliquid 66/74.
## Riferimenti
- Serleto, L. & Malanga, C. — *Pythagoras Trading Prediction* (2024)
- Serleto, L. & Malanga, C. — *Libro dei Frattali* (2024)
## Licenza
Uso privato. Non destinato alla distribuzione.
**Onestà prima di tutto**: nessun numero va creduto finché non è netto fee, out-of-sample, robusto
su griglia, e su dati certificati, liquidi ed eseguibili. Il resto di questo repo esiste per rendere
quella frase verificabile invece che dichiarata.
+8 -5
View File
@@ -10,14 +10,17 @@
"max_data_age_days": 2,
"_nota_skh_feed": "Freschezza del feed 5m usato per il segnale SKH01 (2026-07-26). fresh_5m ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e il certificato si rigenera 1x/giorno: senza controllo la latenza d'uscita di SKH01 passa da ~1h a ~1 giorno senza segnalazione. Sopra soglia book_execute ALLERTA e NON blocca (bloccare fermerebbe anche TP01, nettato sullo stesso strumento, per un guasto di rete). Diario 2026-07-26-t1-esecuzione-skh-live.md.",
"skh_feed_max_age_min": 30,
"_nota_usde": "Collaterale USDE a rendimento (test di eligibilita' 2026-08-26: 500 USDE, verdetto entro il 29/08 — diario 2026-08-26-usde-analisi). L'autorita' che LEGGE questa sezione e' src/live/usde.py; la usano shadow._collaterale_usde (equity oraria del book) e scripts/live/usde_watch.py (sorveglianza giornaliera 12:35 UTC). Criteri delle soglie, dichiarati (P6): depeg_warn 0.99 = fuori dalla banda operativa dello spot (~3 bps) e oltre il clamp +-0.5% per fonte dell'indice usde_usdc — a quel prezzo non e' rumore di book; depeg_crit 0.95 = meta' del buffer di haircut (10%) consumata. quota_target 0.70 = la quota DECISA dall'operatore il 2026-08-30 (la decisione di quota che il gate USDE-01 apriva; anticipata di un giorno sul rinvio al 31/08, su richiesta esplicita dell'operatore \"porta in usde tutto il capitale che non viene usato\"). Il 70% NON e' un argmax (M8): e' il massimo compatibile col CUSCINO DI REGOLAMENTO, cioe' il vincolo che r0830_usde_quota non aveva guardato — il P&L e il funding dei perp USDC-lineari si regolano in USDC, non nel collaterale, quindi a quota alta il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia a interesse. Il cuscino richiesto e' il disaster-SL sulla massima esposizione lorda: n_asset x frac x disaster_sl_pct = 2 x 0.5 x 0.30 = 30% dell'equity, che lascia esattamente il 70%. quota_max_frac 0.50 = tetto di ALLERTA sulla quota USDE/equity totale (N4: la quota e' l'unica leva contro il rischio emittente, -100% = 25 anni di resa, non recuperabile). Alzato a 0.85 il 2026-08-30 in previsione della quota al 70% e RIMESSO A 0.50 lo stesso giorno: il 70% NON e' raggiungibile. 🚨 venue_cap_frac 0.318 = TETTO DEL VENUE sull'USDE, misurato e NON documentato da Deribit (ne' 'Cross collateral specifications' ne' 'Yield-generating collateral' prevedono un limite sulle quantita' detenibili; il Cap ETHENA che esiste diluisce il TASSO a livello di exchange, non limita gli acquisti). Ogni acquisto oltre il tetto e' rifiutato con `not_enough_funds_in_currency` pur avendo $1.400 disponibili: messaggio FUORVIANTE. E' una FRAZIONE dell'equity, non un livello: provato il 31/08 lasciando scendere l'equity di $8, il tetto e' sceso con lei. ⚠️ DIPENDE DAL MODELLO DI MARGINE, ma pochissimo — misurato a saldo neutro sotto entrambi: SEGREGATO S:SM [643,18-644,18) con equity $2.055,56 = 31,29-31,34%; CROSS X:SM [654,18-655,18) con equity $2.054,90 = 31,84-31,88%. Il passaggio a cross ha comprato +0,55pp = **+11 USDE (~$11)**: il modello entra nel tetto, ma non lo spiega. ⇒ la quota resta ~31-32% sotto qualunque configurazione e il 70% di quota_target NON e' raggiungibile (manca un fattore ~2,2x). Il valore 0.318 e' il bordo BASSO del bracket CROSS, che e' il modello attivo: fa fallire il piano PRIMA dell'ordine. Se si torna a S:SM va rimesso a 0.312. haircut 0.05 — ✅ DIVERGENZA CHIUSA il 2026-08-31 (era 0.10, SBAGLIATO). La pagina margini del conto non espone l'haircut come numero: si RICAVA per differenza fra le due righe, perche' il modello CROSS conta l'USDE scontato e il SEGREGATO non lo conta affatto. Contributo dell'USDE al cross $610,81 su $643,05 di valore -> 5,0137%: l'ipotesi 5% torna a $0,09, la 10% sbaglia di $32,06. Riproducibile: `scripts/research/r0831_margini_conto.py` (N11). Il 26/08 il 10% fu registrato come 'verificato sul venue' senza lasciare traccia di come, e non lo era. 🚨 SCOPERTO NELLA STESSA LETTURA, e vale piu' dell'haircut: il modello di margine ATTIVO e' **Segregated: Standard Margin (S:SM)**, NON cross-collateral. Nella tabella del modello attivo l'USDE **non compare**: non fa margine per i perp USDC-settled del book. Percio' l'haircut oggi non si applica affatto — ed e' esattamente il motivo per cui la misura del 31/08 trovava $0,0000 accantonati. Passare a X:SM aggiungerebbe $610,87 di margine utilizzabile: e' una decisione dell'operatore, non un refactor, e porta con se' la meccanica cross (collateral fee 0,05%/giorno sul saldo negativo, ribilanciamento automatico).",
"_nota_usde": "Collaterale USDE a rendimento (test di eligibilita' 2026-08-26: 500 USDE, verdetto entro il 29/08 — diario 2026-08-26-usde-analisi). L'autorita' che LEGGE questa sezione e' src/live/usde.py; la usano shadow._collaterale_usde (equity oraria del book) e scripts/live/usde_watch.py (sorveglianza giornaliera 12:35 UTC). Criteri delle soglie, dichiarati (P6): depeg_warn 0.99 = fuori dalla banda operativa dello spot (~3 bps) e oltre il clamp +-0.5% per fonte dell'indice usde_usdc — a quel prezzo non e' rumore di book; depeg_crit 0.95 = meta' del buffer di haircut (10%) consumata. quota_target 0.70 (CHIAVE TOLTA il 2026-09-10, issue #7: il bersaglio operativo e' DERIVATO dalle bande del cuscino, vedi usde._nota_cuscino — quanto segue e' la storia della decisione) = la quota DECISA dall'operatore il 2026-08-30 (la decisione di quota che il gate USDE-01 apriva; anticipata di un giorno sul rinvio al 31/08, su richiesta esplicita dell'operatore \"porta in usde tutto il capitale che non viene usato\"). Il 70% NON e' un argmax (M8): e' il massimo compatibile col CUSCINO DI REGOLAMENTO, cioe' il vincolo che r0830_usde_quota non aveva guardato — il P&L e il funding dei perp USDC-lineari si regolano in USDC, non nel collaterale, quindi a quota alta il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia a interesse. Il cuscino richiesto e' il disaster-SL sulla massima esposizione lorda: n_asset x frac x disaster_sl_pct = 2 x 0.5 x 0.30 = 30% dell'equity, che lascia esattamente il 70%. quota_max_frac 0.50 = tetto di ALLERTA sulla quota USDE/equity totale (N4: la quota e' l'unica leva contro il rischio emittente, -100% = 25 anni di resa, non recuperabile). Alzato a 0.85 il 2026-08-30 in previsione della quota al 70% e RIMESSO A 0.50 lo stesso giorno perche' il 70% pareva irraggiungibile; RIMESSO A 0.85 il 2026-09-06, quando la quota e' arrivata al 68,7% su autorizzazione dell'operatore e la sonda non ha trovato tetto. 🚨 venue_cap_frac: il 06/09 il tetto NON C'ERA (null, vedi venue_cap_misurato) — quanto segue e' la storia del 30-31/08, tenuta perche' il tetto puo' tornare: venue_cap_frac 0.318 = TETTO DEL VENUE sull'USDE, misurato e NON documentato da Deribit (ne' 'Cross collateral specifications' ne' 'Yield-generating collateral' prevedono un limite sulle quantita' detenibili; il Cap ETHENA che esiste diluisce il TASSO a livello di exchange, non limita gli acquisti). Ogni acquisto oltre il tetto e' rifiutato con `not_enough_funds_in_currency` pur avendo $1.400 disponibili: messaggio FUORVIANTE. E' una FRAZIONE dell'equity, non un livello: provato il 31/08 lasciando scendere l'equity di $8, il tetto e' sceso con lei. ⚠️ DIPENDE DAL MODELLO DI MARGINE, ma pochissimo — misurato a saldo neutro sotto entrambi: SEGREGATO S:SM [643,18-644,18) con equity $2.055,56 = 31,29-31,34%; CROSS X:SM [654,18-655,18) con equity $2.054,90 = 31,84-31,88%. Il passaggio a cross ha comprato +0,55pp = **+11 USDE (~$11)**: il modello entra nel tetto, ma non lo spiega. ⇒ la quota resta ~31-32% sotto qualunque configurazione e il 70% di quota_target NON e' raggiungibile (manca un fattore ~2,2x). Il valore 0.318 e' il bordo BASSO del bracket CROSS, che e' il modello attivo: fa fallire il piano PRIMA dell'ordine. Se si torna a S:SM va rimesso a 0.312. haircut 0.05 — ✅ DIVERGENZA CHIUSA il 2026-08-31 (era 0.10, SBAGLIATO). La pagina margini del conto non espone l'haircut come numero: si RICAVA per differenza fra le due righe, perche' il modello CROSS conta l'USDE scontato e il SEGREGATO non lo conta affatto. Contributo dell'USDE al cross $610,81 su $643,05 di valore -> 5,0137%: l'ipotesi 5% torna a $0,09, la 10% sbaglia di $32,06. Riproducibile: `scripts/research/r0831_margini_conto.py` (N11). Il 26/08 il 10% fu registrato come 'verificato sul venue' senza lasciare traccia di come, e non lo era. 🚨 SCOPERTO NELLA STESSA LETTURA, e vale piu' dell'haircut: il modello di margine ATTIVO e' **Segregated: Standard Margin (S:SM)**, NON cross-collateral. Nella tabella del modello attivo l'USDE **non compare**: non fa margine per i perp USDC-settled del book. Percio' l'haircut oggi non si applica affatto — ed e' esattamente il motivo per cui la misura del 31/08 trovava $0,0000 accantonati. Passare a X:SM aggiungerebbe $610,87 di margine utilizzabile: e' una decisione dell'operatore, non un refactor, e porta con se' la meccanica cross (collateral fee 0,05%/giorno sul saldo negativo, ribilanciamento automatico).",
"usde": {
"index_name": "usde_usdc",
"haircut": 0.05,
"quota_target": 0.7,
"venue_cap_frac": 0.318,
"venue_cap_misurato": "2026-08-31 (X:SM)",
"quota_max_frac": 0.5,
"_nota_cuscino": "Dal 2026-09-10 (issue #7) la quota USDE NON e' un numero scelto: e' DERIVATA dal cuscino USDC di regolamento (n_asset x frac x disaster_sl_pct = 30% dell'equity) e da queste tre frazioni del cuscino, lette da src/live/usde.py per scripts/live/cuscino_watch.py (cron :53). cuscino_margine_frac 0.20 = bersaglio unico di ogni conversione: quota = 1 - 0.30 x 1.20 = 0.64 (il vecchio quota_target 0.70 lasciava slack ZERO: ogni ora in perdita avrebbe venduto). cuscino_preavviso_frac 0.10 = sotto, allerta. cuscino_riacquisto_frac 0.40 = sopra, si RICOMPRA USDE fino al bersaglio (un bonifico alza lo slack di 0,7 x importo e fa scattare il riacquisto da solo). Isteresi [0 ; 0.40] x cuscino con bersaglio a 0.20: oscillare richiede +-6% di equity di slack, cioe' +-8,6% di equity USDC (slack = 0,7 USDC - 0,3 USDE). Soglie DICHIARATE, non ottimizzate (M8): un giro costa ~6 bps sull'importo mosso. Ordine obbligato: 0 < preavviso < margine < riacquisto (usde.bande_cuscino lo verifica).",
"cuscino_preavviso_frac": 0.1,
"cuscino_margine_frac": 0.2,
"cuscino_riacquisto_frac": 0.4,
"venue_cap_frac": null,
"venue_cap_misurato": "2026-09-06: NESSUN tetto fino al 68,7% (fermata dal cuscino di regolamento al 70%, non dal venue): +2.434 USDE in 39 ordini a saldo crescente, zero rifiuti (r0906_usde_tetto_sonda.py). Il 30-31/08 il tetto c'era: [643-644) e [654-655) USDE su ~$2.055 = 31,3-31,9%. Non spiegato: e' SPARITO, non e' scalato. null = nessun tetto noto; usde_convert si affida al chunk+backoff sui rifiuti",
"quota_max_frac": 0.85,
"depeg_warn": 0.99,
"depeg_crit": 0.95
}
+25 -2
View File
@@ -1,7 +1,6 @@
# 2026-09-02 — Debito 14: il report stampa il TWR, non il bonifico
*Scritto il 2026-09-02 fra le 14:16Z e le 14:30Z. Numeri dalla serie `equity` di `trades.db`
(1.678 letture) e dal report rilanciato dopo la riparazione.*
*Scritto il 2026-09-02 fra le 14:16Z e le 14:22Z (commit `2f701b8`, 14:22:50Z; la prima stesura diceva «fino alle 14:30Z» senza averlo letto — corretto in revisione). Numeri dalla lettura di equity delle 13:47:01Z (1.678 letture) e dal report rilanciato dopo la riparazione.*
## Cosa c'era
@@ -51,3 +50,27 @@ nel test, nel CLAUDE.md e qui.
dollari e la sua pagina ha una forma testata. Aggiungerlo è una riga, ma non era il debito.
- Non ho scomposto lo scarto (~$25) fra round-trip + non realizzato e il trading al netto: era già
fuori scope il 01/09 e lo resta.
## Revisione del codice, prima tornata (15:15Z, commit `835e0c8` 15:17:05Z) — tre cose trovate, tutte riparate
| trovato | vero? | riparazione |
|---|---|---|
| l'intervallo che contiene un movimento certo esce intero dal rendimento: il suo P&L di mercato finisce in `certi` e fuori da `trading`, e nessun documento lo diceva | sì, per costruzione. Errore massimo = metà del movimento riconosciuto (margine del classificatore). Il 25/08: ~$263 lordi a ±0,2% → ~$0,5 | dichiarato nel docstring, nel report, in CLAUDE.md §5.14 e in memoria 40. Non si stima (P12) |
| a base di equity zero `trading` era un numero mentre `twr` era None, e sbagliato: il salto 0→X è invisibile al classificatore, `trading` valeva l'intero conto sotto l'etichetta "al netto dei versamenti" | sì. Non raggiungibile dal cron (non scrive mai 0), ma raggiungibile da un backfill | tre stati veri: `twr` e `trading` entrambi None con motivo; il report stampa n/d. Test in `test_journal.py` e `test_trades_report.py` |
| movimento nel primo intervallo o due consecutivi → segmento `+0.00% [x -> x]` | sì, cosmetico | i segmenti a lunghezza zero non si producono; nessun tempo a mercato → TWR 0,0 dichiarato numero. Test |
Il numero pubblicato non cambia: +10,61% alle 14:02Z, +10,56% alle 14:47Z per la marcatura.
## Seconda tornata (15:30Z) — la piu' importante di tutte
| trovato | vero? | riparazione |
|---|---|---|
| il classificatore dei movimenti era **cieco ~23 ore al giorno**: il feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, qualunque calo ≥10% del giorno diventava «movimento». Riprodotto dalla revisione su copia del DB: −12% appeso alle 14:47 → `certi` 1.399 → 1.153, TWR invariato invece di −2,71% | si'. Verificato: ultima barra 00:00, adesso 15:26 | nuovo stato **«mercato non misurabile»** (feed fermo, assente, o barra mancante) ⇒ `ambiguo` con la ragione; `twr` e `trading` None con motivo |
| con `data/raw` assente (host ripristinato prima del rebuild) tutto era «ambiguo», `certi` 0, e il report stampava **+243% sotto l'etichetta TWR** | si' | stesso stato: n/d, mai il grezzo |
| «`rendimento_twr` e' la funzione che entrambi chiamano» era **falso**: `pnl_giorno` rifaceva `cum − certi`, e la testata Telegram mandava il cumulato grezzo | si' | `pnl_giorno` chiama `rendimento_twr`; la pagina stampa il TWR accanto al cumulato; la testata dice «di cui versati → trading» |
| `leva_tetto` del classificatore ridichiarava `frac × n` ignorando `book_scale_k` e `LEVA_LORDA_MAX` (P1) | si' | `min(frac × n × scala, LEVA_LORDA_MAX)`; fallback = tetto di codice |
| il «limite ereditato: +10% di trading a mercato fermo» scritto la mattina era un **artefatto della fixture piatta** (a mercato misurato il trading non supera 2·max_mkt) | si' | cancellato ovunque; il limite vero e' quello sopra |
| il bound di mercato guarda solo BTC/ETH, ma il 31% dell'equity e' USDE: un depeg pieno sarebbe un «prelievo» | si' | **non riparato**: l'indice USDE e' registrato una volta al giorno. Debito **§5.15** |
| l'ora del report scritta in §2 (14:02Z) era l'ora di esecuzione, non quella della lettura (13:47:01Z); i tempi «scritto» dei diari erano posteriori ai commit | si' | corretti coi tempi veri |
Il numero pubblicato: TWR +10,61% alla lettura delle 13:47Z; +10,56% alle 14:47Z.
@@ -0,0 +1,67 @@
# 2026-09-02 — Debito 7: il docstring diceva 230 minuti, il cron gira ogni ora
*Scritto il 2026-09-02 fra le 14:36Z e le 14:38Z (commit `861cc7f`, 14:38:53Z; la prima stesura diceva «alle 14:45Z» senza averlo letto — corretto in revisione). Cadenza misurata dal log (`r0823_sl_anchor.py`: 1.443 giri in
1.442 ore) e dalla crontab installata, letta dal test.*
## Il difetto
| fonte | cosa diceva | dal |
|---|---|---|
| `scripts/live/book_execute.py`, docstring | «va lanciato ogni ~230 minuti» | 20/06 |
| `scripts/cron_book.sh`, intestazione | cadenza ORARIA, `47 * * * *` | 25/08 (prima `:07`, sempre orario) |
| crontab della VPS | `47 * * * *` | 25/08 |
Era il **docstring** a sbagliare. E non era una svista innocua: sulla riga 4h (la più vicina ai 230
minuti) BTC **raddoppia** gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5, con
2 in 30 giorni). Il rischio era un lettore diligente che "allineasse" il cron al docstring.
## La riparazione
1. **Docstring riscritto**: `CADENZA: ORARIA`, la riga di crontab citata, la ragione (giro
idempotente → girare più fitto della griglia non costa ordini; latenza ≤1h sugli ingressi/uscite
software di SKH01; ri-ancoraggio orario del rotolante) e il divieto esplicito, con la data.
2. **`tests/test_book_cadenza.py`, 10 test.** P1: non ridichiara «60 minuti», **deriva** le tre
fonti e le confronta fra loro:
- il docstring non contiene più «ogni ~230» e dichiara ORARIA;
- la riga di crontab citata nel docstring è quella dichiarata in `cron_book.sh`;
- la riga dichiarata è oraria (parser a 5 campi, solo casi regolari, rifiuta il resto),
sotto i 230 della griglia e lontana dai 240 della riga peggiore, e fuori dal minuto `:00`;
- **la crontab installata** (`crontab -l`) coincide con la dichiarata. Dove la crontab non è
leggibile il test è **saltato**, non verde (P5). Sulla VPS oggi gira e passa.
## Numeri
| | |
|---|---|
| test nuovi | 10, tutti verdi; suite dei moduli che toccano `book_execute`: 231 verdi |
| cadenza dichiarata / installata | 60 min / 60 min |
| griglia SKH01 | 230 min (la riga da non usare) |
| riga peggiore misurata | 240 min (BTC 2 scatti contro 1) |
## Cosa NON ho fatto
- Non ho toccato il cron né `cron_book.sh`: la configurazione che gira era quella giusta.
- Non ho aggiunto un sorvegliante a runtime sulla cadenza (contare i giri per ora nel log): il
giornale già stampa «giri mancanti» ogni giorno, e il test copre la deriva di configurazione.
## Revisione del codice, prima tornata (15:15Z, commit `835e0c8`) — due cose trovate, tutte riparate
| trovato | vero? | riparazione |
|---|---|---|
| il docstring nuovo diceva «il disaster-SL rotolante si ri-ancora ogni ora»: falso. `ensure_disaster_sl` lascia il bracket com'è finché lo stop voluto è entro il 5% da quello piazzato e la taglia entro il 10%; il giro orario **controlla**, ri-ancora oltre la tolleranza (mark +5,263% / −4,762%). I «BTC 0 / ETH 1» di `r0823` sono misurati **con** questa isteresi | sì, verificato in `execution.py:233-235`. Propagato anche a CLAUDE.md §5.7 e memoria 40 | riscritto in tutti e tre i posti: «controllato ogni ora, ri-ancorato solo oltre la tolleranza; lo stop siede fra −26,3% e −33,3% dal mark corrente». Frase sbagliata su un meccanismo di sicurezza vivo: era la segnalazione più importante |
| la guardia del test cercava due letterali («ogni ~230»): «ogni quattro ore» sarebbe passato | sì | la guardia estrae **ogni** prescrizione «ogni/every N unità» e pretende 60 minuti; controllo positivo (M15) su cinque frasi. Limite dichiarato (P13): una prosa che prescrive senza «ogni» passa |
## Seconda tornata (15:30Z)
| trovato | vero? | riparazione |
|---|---|---|
| la frase nuova di CLAUDE.md §5.7 era **invertita** («chi lo avesse corretto nel verso del cron»): correggere il docstring verso il cron e' proprio la riparazione; il pericolo e' correggere il CRON verso il docstring | si' | riscritta nel verso giusto |
| la banda dello stop era sbagliata: con isteresi 5% sullo stop (0,70·mark) lo stop siede a 0,665–0,735 × mark, cioe' **−33,5% / −26,5%**, non −26,3/−33,3 | si' | corretta in docstring, CLAUDE.md, memoria |
| «BTC raddoppia gli scatti» leggeva male `r0823`: il «2 contro 1» e' rotolante contro pavimento **dentro la riga 4h**; a 1h il rotolante ne fa 0, e la riga peggiore misurata e' **24h** (ETH 5) | si' (la revisione ha eseguito `r0823.simula`) | riscritto ovunque coi conteggi; nel test `RIGA_4H_MIN`, non «peggiore»; `LTF_MIN` importato da `skyhook` (P1) |
| «girare piu' fitto non costa ordini» era falso: 22 dei 48 ordini live sono ri-taglie |Δ|≤$10 a segnale invariato; il deadband e' in valuta assoluta (C2) | si' (contati) | riscritto: «costa solo micro-ordini di ri-taglia», con la regola C2 |
| «latenza ≤1h» senza condizione: vale solo con la feed 5m fresca; `fresh_5m` ripiega in silenzio sul giornaliero e `skh_feed_max_age_min` allerta e basta | si' | condizione scritta |
| `_cron_installato` leggeva la **prima** riga: una seconda riga attiva sotto (`17 * * * *`, dentro lo slot di release) era invisibile | si' (simulato) | si contano **tutte** le righe attive, e devono essere una |
| lo skip collassava tre stati: crontab illeggibile, leggibile senza il progetto, leggibile col progetto ma senza `cron_book.sh` attivo (= libro non schedulato) | si' | tre stati: SALTATO, SALTATO, **ROSSO** |
| solo il vincolo 1 del `:47` era testato (minuto ≠ 00); il vincolo 2 (fuori dallo slot di release del martedi') no | si' | test con `venue_probe.in_release_window`, il :07 come controllo positivo |
| `_cron_dichiarato` accettava solo `N * * * *` e falliva come «assente» su `*/30`; `cadenza_minuti` accettava `*/45`, `0 */5`, `*/0` | si' | regex a 5 campi qualunque, regolarita' e passo > 0 imposti, parametrizzati |
| `r0823_sl_anchor.py` si rifiutava di girare nella finestra del **vecchio** `:07` e stampava a runtime che il docstring «prescrive ~230 min»; `cron_chain.sh` giustificava il :25 «fuori dal :07» | si' | guardia sul :47, prosa al passato, commento aggiornato |
+103
View File
@@ -0,0 +1,103 @@
# 2026-09-02 — Un flag sconosciuto non è l'azione di default
*Scritto fra le 15:45Z e le 16:05Z. Il difetto è stato misurato, non ipotizzato: la revisione del
codice di questa sessione ha eseguito `trades_db.py --help` come sonda di tempi e lo ha visto
sincronizzare il DB.*
## 1. Il difetto
Nessuno dei 18 script di `scripts/live/` usava argparse. Leggevano gli argomenti così:
```python
if "--reconcile" in args: ...
elif "--report" in args: ...
else: sync() # <- qui finiva QUALUNQUE cosa non riconosciuta
```
Un flag sbagliato non era un errore: era il ramo finale. Costo osservato e costi possibili:
| script | `--help` faceva | gravità |
|---|---|---|
| `trades_db.py` | `sync()`: riscriveva `meta.ultimo_sync` | osservato oggi, danno nullo |
| `journal.py` | scriveva la pagina di oggi e una riga nel DB | non osservato, ma scrive |
| `analista.py` | spendeva una chiamata al modello e mandava un Telegram | non osservato, ma spende |
## 2. La riparazione, e perché non è argparse
`src/live/cli.valida(nome, uso, flag=..., con_valore=...)`, chiamata come **prima istruzione** di
ogni blocco `__main__` — prima di `connect()`, prima del sync, prima della rete.
- `--help` / `-h` → stampa l'uso, esce **0**;
- flag non dichiarato, o valore mancante, o posizionale → stderr con **cosa** e **cosa si poteva**
(P4), esce **2** (errore d'uso, distinto dall'1 con cui gli script segnalano un esito negativo);
- tutto il resto invariato.
**Argparse no.** Cambierebbe messaggi d'errore, codici d'uscita e il comportamento di `--help` su
18 script che il cron già invoca: romperebbe ciò che gira per riparare ciò che non gira mai. Qui
serviva l'opposto — lasciare intatti i flag esistenti e rifiutare solo l'ignoto.
## 3. Il test guarda entrambe le metà del contratto
`tests/test_cli_flag.py`, 30 test:
- **l'elenco degli script si deriva dalla cartella** (P1): uno script nuovo che legge `sys.argv` a
mano e non chiama `valida` fa fallire il test, non passa inosservato perché nessuno ha aggiornato
una lista;
- `valida` dev'essere la **prima istruzione** di `__main__` (uscire dopo l'effetto non è uscire);
- l'uso deve **nominare** i flag che lo script accetta;
- **i flag che il cron usa davvero devono restare accettati** (P15/P16, letti dai `cron_*.sh`): una
guardia che ferma il libro di bordo la notte stessa sarebbe peggio del difetto;
- end-to-end sui tre script che scrivono: `--help` esce 0 e **non tocca `trades.db`**; un flag
ignoto esce 2 e non lo tocca (M15: il caso che ha causato il difetto, non un caso vicino).
Verificato a mano anche il contrario: `monitor_health.py --quiet`, `trades_db.py --sync --quiet`
e `book_execute.py` in dry-run escono 0 come prima.
## 4. Il resto della sessione
**Indice USDE orario** (debito §5.15, primo passo). Il classificatore dei movimenti di capitale
confronta due letture di equity a un'ora di distanza e il suo bound guarda solo BTC/ETH, mentre il
31% dell'equity è USDE: un depeg sarebbe scorporato come un prelievo. `usde_watch` registra
l'indice una volta al giorno — troppo rado. Ora `balance_watch` (orario, sola lettura) lo registra
a ogni campione, `None` con la ragione se non leggibile, mai 1,0. Il cablaggio nel bound viene
quando la serie ha storia: prima serve il dato, poi la regola.
**Pulizia lasciata dalla revisione.** `tests/helpers.carica_script` sostituisce la dodicesima copia
del caricatore `importlib` (15 in giro, con nomi di modulo diversi per lo stesso file e differenze
silenziose su `sys.modules`); il fill di prova si scrive con `upsert_fills` invece che con un
`INSERT` a mano che lasciava `verified` NULL, una forma che la produzione non produce; le
asserzioni sul testo del report non sono più ancorate al padding; `movimenti_capitale` accetta le
righe già lette, così il report non fa due SELECT sulla stessa tabella a un'ora del cron — due
letture ai due lati di una scrittura descriverebbero due istanti diversi.
## 5. Il README era fermo a quindici giorni prima del reset
Chiesto un aggiornamento della documentazione, ho aperto il README e ho trovato un documento del
**2026-06-04**. Il reset v2.0.0 è del **19 giugno**. Per 75 giorni la prima pagina del repository ha
descritto la libreria pre-reset come se fosse il presente:
| cosa diceva | stato vero |
|---|---|
| famiglie FADE / HONEST / PAIRS / TSMOM / SHAPE, strategie MR01…SH01 | archiviate in `Old/`, artefatto di feed contaminato |
| PORT06: Sharpe **7,84 / 10,06**, DD 2,60%, CAGR ~79% | numeri che `CLAUDE.md` §2 elenca sotto «non citare» |
| paper trader su `strategies.yml`, portafogli in `portfolios.yml` | **file inesistenti** |
| `scripts/waste/`, `scripts/portfolios/`, `src/live/multi_runner.py` | **inesistenti** |
| esecuzione shadow su Deribit **testnet** | il testnet è *la causa del reset*; oggi si esegue su mainnet con soldi veri |
Riscritto contro lo stato vero: cosa gira adesso, i numeri nella lente di §2 (TWR +10,6%, non la
crescita del conto), il metodo e i sei requisiti, la struttura verificata file per file, i gate con
le loro date, l'obiettivo con la sua onestà. Ogni percorso citato è stato controllato: esiste.
Da 423 righe a 152.
**La lezione che ho registrato non è «aggiornare il README».** È che un reset invalida anche i
documenti che nessuno rilegge, e l'inventario di cosa cita numeri morti va fatto il giorno del
reset — non 75 giorni dopo, per caso, mentre si fa altro.
## 6. Cosa NON ho fatto
- I loader `importlib` dei test sulla **ricerca** (moduli in `scripts/research/`) restano come
sono: sono a livello di modulo, con radici diverse, e toccarli non ripara niente.
- Il bound USDE nel classificatore: manca la storia, non il codice.
- Non ho cercato altri documenti fermi al pre-reset fuori da `README.md` e `docs/`: l'inventario
completo (ogni file che cita un numero morto) resta da fare, ed è la vera coda di questa scoperta.
+91
View File
@@ -0,0 +1,91 @@
# 2026-09-06 — USDE: dal 14,6% al 68,7%. Il tetto del venue del 30-31/08 non c'era piu'; il versamento di prova del 25/08 dichiarato
**Ore:** 20:56-21:16Z. **Autorizzazioni dell'operatore, in sessione:** *«usa piu' USDE se ne hai bisogno»*,
poi *«cerca % massima raggiungibile in USDE»*, sulla decisione gia' presa il 30/08 (quota bersaglio 70%,
`usde.quota_target`). E, a meta' sessione: *«i 25 euro erano un versamento di prova»*.
## 1. Il versamento del 04/09 aveva dimezzato la quota
Il versamento di **$2.410,14 del 04/09 14:47Z** (riconosciuto dal classificatore: mercato max 0,56% a leva
piena contro un salto del +116,8%) ha portato l'equity a **~$4.495** e la quota USDE da 31,3% a **14,6%**.
Nessun automatismo la riportava su.
## 2. Primo passo: al tetto di config (31,8%) — e la lettura SBAGLIATA che ne ho dato
`usde_convert.py --quota 0.318 --esegui`: 774 USDE in 8 ordini @ 1,0005 medio, zero rifiuti, quota 31,79%.
Ho scritto in CLAUDE.md «il tetto ha tenuto la stessa frazione a equity 2,2× maggiore: terza conferma».
**Falso**: nessun rifiuto fino al 31,8% dimostra che il tetto e' **≥** 31,8%, non che e' **=** 31,8%. Il
dato non conteneva la conferma; l'ho letta perche' me l'aspettavo. Corretto venti minuti dopo, quando
l'operatore ha chiesto la % massima e la sonda ha mostrato che la conferma non c'era.
## 3. La sonda: nessun tetto fino al 68,7%
`scripts/research/r0906_usde_tetto_sonda.py` — a saldo crescente, passi piccoli, stesso metodo dei diari
30-31/08 (un probe unico di taglia sbagliata produce un falso negativo): sale finche' il venue non rifiuta,
ripete il rifiuto dopo aver riletto il book (il messaggio `not_enough_funds_in_currency` e' lo stesso di un
limite che non incrocia), scende di passo a ogni doppio rifiuto, e conferma il muro a saldo neutro
(SELL 1 → BUY 1 ok → BUY 1 rifiutato). Guardie: prezzo dal book (ask+5 tick, tetto 1,0010), quota mai oltre
il 70% e **USDC mai sotto il cuscino di regolamento** derivato da config (`usde_convert.cuscino_richiesto_usd`).
| corsa | passi | da → a (USDE) | quota | rifiuti |
|---|---|---|---|---|
| 21:02-21:09 | 30 × 2 | 1.428,7 → 1.488,7 | 31,8% → 33,1% | **0** (fermata dal mio limite +60) |
| 21:11-21:15 | 16 × 100 | 1.488,7 → **3.088,7** | 33,1% → **68,7%** | **0** (fermata dal cuscino: +100 avrebbe superato il 70%) |
**Totale del giorno: 2.434 USDE in 39 ordini @ 1,0005, ~$1,2 di spread.** Conto finale: USDC **$1.407,40** +
USDE **3.088,72** = $4.496; cuscino richiesto $1.349 → slack **+$58**. Registro: `data/live/usde_convert.jsonl`
(due record `sonda_tetto`, con ogni ordine).
## 4. Cosa dice il risultato — e cosa non dice
1. **Il tetto del 30-31/08 era vero allora ed e' sparito ora.** Tre bracket riproducibili a 1 USDE (31,21-31,26% ·
31,29-31,34% · 31,84-31,88%) contro 2.434 USDE accettati oggi senza un rifiuto. **Non e' scalato, e'
sparito**: la frase del 31/08 «il 70% non e' raggiungibile ne' ora ne' mai» e' falsificata.
2. **Non spiegato** (D5). Ipotesi non verificabili da qui (il gateway non espone il transaction log ne' i
parametri del cap): un'allocazione per-conto del Cap ETHENA che varia col saldo aggregato dell'exchange;
un vincolo sui fondi depositati di recente (ma l'USDC del 04/09 e' stato convertibile a 2 giorni); un
cambio di policy. **Il tetto puo' tornare**: quando tornera', bloccherebbe gli ACQUISTI, mai le vendite
(tutte le SELL sono sempre passate). `venue_cap_frac` → **null** con la misura in `venue_cap_misurato`;
`usde_convert` si affida al chunk+backoff sui rifiuti, che e' cio' che ha sempre fatto.
3. **Il limite che morde e' NOSTRO: il cuscino di regolamento.** P&L e funding dei perp si regolano in USDC;
a 68,7% restano $58 sopra il cuscino. ⚠️ **Nessuno sorveglia il cuscino**: `usde_watch` allerta sulla
quota (`quota_max_frac`, riportata a **0,85**, il valore che l'operatore aveva scelto il 30/08 «in previsione
della quota al 70%»), non sull'USDC. Una perdita del libro fa salire la quota da sola: a −$58 il cuscino e'
scoperto, e Deribit finanzia un saldo USDC negativo allo 0,05%/giorno (18%/anno). Debito nuovo, §5.17.
4. **Rischio emittente**: a 24,2% l'evento Ethena valeva 3,1× il maxDD del libro (30/08); a 68,7% vale **~8,8×**.
E' la decisione del 30/08 presa con l'informazione completa (r0830_usde_quota), oggi eseguita. Resa attesa
~4,1% × $3.089 ≈ **$127/anno** (era $26): ancora meno di un mese di versamento.
## 5. Il versamento di prova del 25/08: dichiarato, scorporato, e il TWR scende di 4 punti
Trovato stamattina per differenza (equity 07:07→08:07 **+$21,11** con le posizioni a **−$3,3/−4,3**) e
confermato dall'operatore: **€25 di prova** prima del bonifico da $1.399,39 delle 11:47. Il rilevatore non
poteva vederlo (+3,3% contro soglia 10%) e abbassare la soglia farebbe di ogni ora di mercato un candidato:
**l'unica fonte che sa l'importo e' l'operatore**. Costruito il canale:
- `data/live/movimenti_dichiarati.jsonl` (append-only, nel perimetro di backup — P11), letto da
`journal.movimenti_dichiarati()`; `movimenti_capitale` lo fonde coi rilevati: fonte `dichiarato`, importo
dell'operatore (non il salto), `dopo = prima + importo` cosi' il mercato dell'ora **resta** nel rendimento;
se coincide con un salto rilevato l'importo dichiarato vince (`dichiarato+rilevato`); fuori dalle letture →
`avvisi`, non applicato; riga rotta → `avvisi`, le altre restano (P3).
- Importo registrato **$24,90 ± 0,50**: stimato per differenza (salto +21,11, mercato −3,3/−4,3 dal log del
book, fee 0,03) — il gateway non espone `get_deposits`, quindi il numero USDC esatto non e' leggibile; la
banda sta nel record e nel report (P12).
- `tests/test_journal.py`: +6 (scorporo sotto soglia · dichiarato+rilevato · fuori letture · riga rotta ·
senza file niente cambia · la nota di giornale lo dice); `conftest` isola il file nei test.
| | prima (20:47Z) | dopo (21:14Z) |
|---|---|---|
| movimenti certi | $3.809,53 | **$3.834,43** |
| trading da arming | +$87,35 | **+$62,45** |
| TWR | +11,98% | **+7,83%** = +8,14% × −0,62% × −0,12% × +0,46% |
**Un terzo del «trading» era un bonifico da 25 euro.** Regola nuova (CLAUDE.md §2): ogni versamento, anche
di prova, si dichiara nel file il giorno stesso.
## 6. File toccati
`config/live.json` (usde: venue_cap_frac null, quota_max_frac 0.85, nota) · `src/live/journal.py` ·
`scripts/live/trades_db.py` · `tests/conftest.py` · `tests/test_journal.py` (+6) ·
`scripts/research/r0906_usde_tetto_sonda.py` (nuovo) · `data/live/movimenti_dichiarati.jsonl` (nuovo) ·
`CLAUDE.md` §1 §2 §4 §5.
@@ -0,0 +1,219 @@
# 2026-09-09 — Guadagnare nei crolli: revisione dei sistemi, il crollo catturato, piu' monete su Deribit
**Richiesta dell'operatore:** *"fai analisi e revisione dei sistemi adottati e trova soluzione per
guadagnare anche nei crolli (usa le opzioni se serve), anche + monete ma sempre in deribit"*.
Analisi e revisione col modello `fable` (revisore fresco, agente separato), test con Opus.
**Esito in una riga:** nel **giorno** del crollo il libro live **perde** (−0,48%/g, positivo nel 18%
dei giorni ≤ −5%); per **finestra** e' immune-o-positivo e **dal 2022 chiude in utile 8/8 peggiori
finestre**, tutte per la gamba SHORT di SKH01 marcata all'uscita; la sola leva che aumenterebbe quel
guadagno e' il peso di SKH01, che il gate del progetto boccia su una condizione in-sample; le
opzioni, misurate per la prima volta su un **crollo catturato a quote vere**, danno un rapporto di
credito identico al rally (f_net 0,74 vs 0,714) — **non** il `f` di stress che la regola del 19/06
aspettava (crollo a vol bassa, gate di VRP01 chiuso, famiglia inverse); XRP, l'unica "altra
moneta" liquida e non misurata di Deribit, **diluisce** esattamente come SOL. **Libro, pesi,
config, cron INVARIATI. Nessun ordine.**
## 0. Prima di misurare: cosa era gia' morto (memoria letta, non riaperta)
| meccanismo "guadagnare nei crolli" | esito | perche' e' morto (chi riapre deve battere questo) |
|---|---|---|
| gamba short del trend a 1d (§42) | SCARTATO | −1,91%/anno di drift in 0/24 ancore; il 2022 lo tiene a galla a meta' |
| put deep-OTM statica (§46) | REFUTATO 0/162 | beta del libro +0,076: non si assicura un libro gia' piatto |
| collar ≤15g (§71) · pavimento come licenza (§72) | REFUTATI | il DD lo riduce il TETTO (troncatura, C9); la put sola peggiora il DD 16/16 |
| gamma scalping long-vol | SCARTATO | paga il VRP ogni anno, ogni frequenza |
| DVOL direzionale · gate macro · skew de-risk | HEDGE / RIDONDANTI | TP01 e' gia' flat nei crolli (Δ 0,00) |
| SOL terza gamba (22/08) | ESCLUSO dall'operatore | hold-out −0,166 in 0/24; un anno solo (2023) |
| XS01 su Deribit (§50) | MORTO per ampiezza | 11 gambe con ≥1 anno: 1,265 → 0,116 |
Condizione di riapertura scritta tre volte (§3 short-vol, §71, §72): **un `f` di stress reale su
un crash catturato**. E' la cosa che oggi si poteva misurare e non era stata misurata.
## 1. Il libro nei crolli — `r0909_libro_nei_crolli.py`
Lente: sleeve di ricerca, ancora canonica, fee 0,10% RT, funding fuori; pesi importati da
`src/live/book` (P1). ⚠️ **SKH01 e' marcato all'USCITA del trade** (`harness.backtest_signals`):
la serie giornaliera e' zero nell'87,7% dei giorni, e uno short aperto nella finestra e chiuso
fuori e' accreditato fuori — per questo la tabella per GIORNO mostra SKH01 ≈ 0 e quella per
FINESTRA il guadagno. Indice 50/50 BTC/ETH; giorno di crollo ≤ −5%; episodio = 12 peggiori
finestre di 20 giorni non sovrapposte + 9 episodi nominati prima. Classi ESCLUSIVE nell'ordine
dichiarato: immune (|libro| < |idx|/10, col segno) → POSITIVO → PERDE.
| misura | valore |
|---|---|
| beta del libro all'indice | **+0,0769 sulla finestra di §46** (2021-03-24+; memoria +0,076: riprodotto al millesimo) · +0,086 su tutta la storia · +0,002 nei giorni ≤ −3% |
| 160 giorni ≤ −5% (indice −7,76%/g) | libro **−0,48%/g**: TP01 −0,63, SKH01 +0,15; **libro > 0 nel 18%** dei giorni; somme per anno: 2024 −11,9%, 2025 −7,3%, 2022 +8,2%, 2026 +4,7% |
| 24 giorni ≤ −10% (indice −14,4%/g) | libro −0,51%/g: TP01 −0,93, SKH01 **+0,42** |
| 12 peggiori finestre 20g | **4 POSITIVO / 6 immune (4 col segno +) / 2 PERDE**; libro > 0 in 8/12; **dal 2022 in 8/8** (mediana +3,3% contro indice −29%); le 2 perdite sono 2019 e maggio 2021 (TP01 ancora long: il trend non si era girato) |
| 9 episodi nominati | libro > 0 in LUNA (+4,3%), giugno 2022 (+3,1%), FTX (+0,7%), agosto 2024 (+0,4%), 5/02/2026 (+4,4%), 1-5/06/2026 (+3,3%); COVID −0,04%, feb 2025 −1,5%, maggio 2021 −3,4% |
| da dove viene il guadagno | la gamba **SHORT di SKH01 = 108%** dei guadagni negli episodi con libro > 0 (scomposizione ESATTA: long + short == intera su 2735/2735 giorni, verificato con assert; TP01 contribuisce in negativo); per anno al peso 0,25: 2022 **+10,7%**, 2026 **+7,7%** |
| peggior giorno del libro (lente canonica) | 2025-10-10 −3,38%, tutto TP01 con esposizione **0,505**, indice −9,7%: **e' un giorno di crollo con TP01 mezzo long** (lo squeeze di §33 sta sulla lente hourly e qui non si riproduce: dichiarato nel docstring) |
| riga informativa 37,5/62,5 (§26) | batte il 75/25 in 12/12 episodi, **ma** Sharpe 1,81 vs 1,82 e maxDD 12,1% vs 9,4%: paga i crolli con gli squeeze |
🚨 **Errore catturato in revisione (fable):** la prima stesura provava "GUADAGNA" prima di
"immune" e stampava **8/12 GUADAGNA**; con le classi nell'ordine dichiarato sono **4 POSITIVO +
6 immune**. *L'ordine delle regole era il verdetto.* Ora il verdetto e' tre affermazioni separate
(giorno / finestra / provenienza) e il conteggio nudo libro > 0 e' stampato accanto.
📌 **Il meccanismo "guadagnare nei crolli" esiste ed e' in produzione: e' SKH01 short** — ma
guadagna **dopo** il giorno del crollo (marcato all'uscita), non dentro. Nel senso stretto della
domanda dell'operatore il libro **perde nel giorno** e recupera nella finestra. Le due perdite
storiche per finestra sono di TP01 nei crolli che arrivano *dentro* un trend rialzista (2019,
2021): e' il costo del long-flat (§42 ha misurato che la short del trend lo aggrava).
**La leva che aumenterebbe il guadagno e' il peso di SKH01, e il gate la boccia:**
`r0726_reeval_live_weight.py` rilanciato: argmax 0,35, plateau [0,25-0,50] (il live e' dentro),
`weights_tilt_null` 0,25→0,35: `delta_insample −0,0026`, `pctl_hold 31,2`, **`gate_pass False`**.
⚠️ Precisazione della revisione: lo script legge la cache `data/_cache/skh_sigtab/*` **ferma al
2026-07-25** — sono le stesse cifre del diario 26/07, non un ri-esame coi segnali di oggi. La
condizione che fallisce e' pero' **in-sample (pre-2025)**, che 46 giorni di dati nuovi non possono
cambiare: la decisione 75/25 resta vincolante e la sua condizione di riapertura resta non
soddisfatta, ma la data da citare e' il 26/07.
## 2. Il crollo catturato — `r0909_crash_catturato.py`
**Fatto nuovo:** `bite_archive.parquet` copre con bid E ask il crollo del **1-5 giugno 2026**
(BTC −25% / ETH −30% dal 31/05 al minimo; ETH −11,1% il 05/06) su una scadenza (19/06, 14-21
DTE). Il campione del 22/08 (§11) partiva **dopo**, dal fondo. Limiti dichiarati prima: n = 1
crollo, 1 scadenza, tenor 14-21 g (VRP01 e' a 7), spot dal feed 1h certificato, regolamento allo
spot dell'ora di scadenza, **famiglia INVERSE** (premi in ETH: VRP01 opererebbe sulla USDC-lineare,
che il conto margina e che NON e' nell'archivio). Ancora d'ingresso = ogni snapshot 30/05-04/06;
mediana e banda.
⚠️ **Non e' un crollo a vol alta:** DVOL pre 37 (BTC) / 49 (ETH) all'IV-rank 0,05 / 0,10; il
**picco** del crollo (49 / 69) sta al **29° / 46° percentile storico** — sotto la mediana. Il gate
IV-rank ≥ 0,30 di VRP01 e' **chiuso in tutte le ore** e il modello del sleeve segna 0,0 su tutte le
settimane di giugno: VRP01 non sarebbe stato a mercato.
| ETH, ingressi PRE-crollo (286 snapshot = **4 strutture** di strike, δ −0,27/−0,09, larghezza 200) | reale | modello (BS su DVOL) | f |
|---|---|---|---|
| credito del put credit spread | | | **0,74** [0,65-0,82] — 22/08 in rally: 0,714 |
| P&L a scadenza per unita' | **−$174** | −$168 | 1,04 — **tautologico nell'83%** dei trade: entrambe le gambe ITM ⇒ payoff = larghezza per reale e modello, resta solo f_net |
| perdita sul rischio (fee incluse) | 101% [65-102] | | = perdita **MASSIMA** dello spread (+ fee); 65% per la 1900/1600 |
| peggior MTM, chiusura **tagliata alla larghezza** | **−$172** | −$161 | **1,08** [1,07-1,31]; a quell'ora IV corta 68 = DVOL 68 |
| put δ−0,10 comprata all'ask: costo | | | **1,92×** il modello (§46 a δ−0,10 su BTC_USDC: 1,27 — punto NUOVO e piu' alto, ETH inverse) |
| put δ−0,10 a scadenza (ITM di $4: K 1700, ST 1694) | −$6 | ≈ $0 | 0,96 sul solo sottoinsieme con payoff modellato ≥ $5 |
| put δ−0,10 al picco del percorso (look-ahead, limite sup.) | +$163 | +$173 | 0,94 |
| debit spread −0,28/−0,10 a scadenza | +$169 | +$163 | 1,04 (stessa tautologia, specchio) |
| ingressi DURANTE il crollo (02-04/06, 283 snapshot) | mediana +$5, **perde nel 46%**, p10 −$133 / p90 +$26 | | bimodale: la mediana nasconde le perdite |
BTC: **non misurabile pre-crollo** — la griglia di strike dell'archivio il 30/05-01/06 non
contiene la δ−0,10 (78 snapshot fuori geometria, 0 in geometria prima del 02/06). Durante:
247 snapshot, f_net 0,79, perde nel 37% (p10 −$1.718 / p90 +$818 su larghezza 5000).
🚨 **Tre correzioni della revisione, applicate:**
1. *"Il percorso costa 2,6× il modello (f_mtm 2,58), la IV della gamba corta e' esplosa a 108 per lo
skew"* — **artefatto di quote.** Il costo di chiusura `(ask corta − bid lunga)` superava la
**larghezza** dello spread (una quota 0,208/0,376 ETH su un intrinseco di $296: 158% di spread
relativo, a T→0) e `min()` pescava li'; uno spread a rischio definito non costa mai piu' della
larghezza per chiudere. Tagliato: **f_mtm 1,08**. E la IV 108 e' del **18/06 alle 17:00** (deep
ITM, T→0), non del crollo: all'ora del peggior MTM la IV e' 68 = DVOL.
2. *"f a scadenza 1,04: il modello prezza bene la perdita"* — **tautologia**: con entrambe le gambe
ITM il payoff e' la larghezza per entrambi; f_exp e' una funzione di f_net. E la "banda su 286
snapshot" sono 4 strutture (N_eff = 1 crollo × 4).
3. `S.asof(ts)` sul feed 1h **guardava un'ora avanti**: la barra e' etichettata all'apertura (la
20:00 chiude col 5m delle 20:55). Corretto con `spot_causale` (indice +1h). ⚠️ **`cblib.spot_series`
ha lo stesso difetto** → il f 0,714 di §11 e ogni numero di `cblib` con `asof` sullo spot; qui
innocuo (ambo le gambe ITM per ogni ST < 1700) e nel MTM giocava contro l'autore. **Debito nuovo,
registrato in CLAUDE.md §5.** Non toccato: cambierebbe numeri pubblicati.
📌 **Cosa dice davvero il crollo catturato (n = 1):** il **rapporto di credito reale/modello non
cambia col regime** (0,74 nel crollo contro 0,714 nel rally); la put comprata paga nel crollo
cio' che il modello dice (0,94-1,04) ma **costa 1,9× il modello all'ingresso** — il bleed e' dove
il modello sbaglia, il payoff no (e' il motivo di §46/§72 visto dal lato del sinistro); il gate di
VRP01 lo ha tenuto fuori. **La condizione di riapertura di §3 e' soddisfatta nella forma** (un
archivio bite con bid/ask su un crollo) **e non nella sostanza**: crollo a vol bassa, sotto il gate,
tenor 14-21, famiglia inverse, e l'unico f non tautologico (MTM) vale 1,08 su 4 strutture. **Non si
riapre.** Primo punto di una distribuzione che `collect_chain` riempira' da solo; il secondo punto
utile e' un crollo **con IV-rank > 0,30**.
## 3. Piu' monete, sempre in Deribit — `r0909_deribit_universo.py` · `r0909_xrp_terza_gamba.py`
**Il venue letto oggi (15:05Z):** 32 perpetual USDC-lineari aperti (+6 non aperti, fra cui **APT
inactive**: e' il mancante rispetto al 23/08). Volume 24h ≥ $10M solo per **XRP ($38M, il piu'
scambiato del venue, spread 4 bps, listato 2022-03-16), ETH, BTC, SOL ($13M)**; tutto il resto fra
$0,01M e $7M (TAO, HYPE, ADA, AVAX, NEAR…) con spread 2-20 bps. XS01: **13/19** gambe — mancano
ARB, OP, APT, INJ, TIA, SEI → §50 resta: morto per ampiezza. Opzioni USDC-lineari su 7 sottostanti
(SOL 676, HYPE 442, XRP 442, TRX, AVAX) con la liquidita' misurata il 22/08 (SOL: 9% delle put a
due lati). Trovati dalla revisione, registrati perche' il controllo costa €0 e nessuno li riapra:
**`BTCDVOL_USDC-30SEP26`** (l'unico strumento long-vol diretto del venue: volume 0, OI 132, bid/ask
39,1/43,9 = spread 12% → non negoziabile, e in contango sanguina come la put) e **PAXG perpetual**
(oro, dal 2024-12: $0,18M/g, OI $8,3M — non misurato, non morto, stessi muri di SOL/XRP: 9 mesi di
storia contro i 3 anni della decisione 22/08).
⇒ **L'unica "altra moneta" liquida su Deribit mai misurata come gamba direzionale e' XRP.** Misurata
col harness di `r0822_sol_leg` (TP01 e SKH01 **congelati**, 24 ancore, differenze appaiate,
de-levering, per anno), ipotesi a priori dichiarata: diluisce.
Certificazione (Coinbase dal rilisting 2023-07, Bitstamp prima — Coinbase aveva delistato XRP per
la causa SEC): >1% dal riferimento 0,06% (2022) · **0,57% (2023)** · 0,01-0,02% (2024+); mediana
5-6 bps. ⇒ **L-PULITA dal 2024** (stessa soglia e stesso anno sporco di SOL). ⚠️ Barre 5m flat
**27-61%** (BTC/ETH 2021+: 0,1-2%; SOL 21%): il 5m su cui gira SKH01 e' 20-100× piu' flat.
| | L-FULL (2022-03+) | L-PULITA (2024+) |
|---|---|---|
| dSharpe FULL (mediana, 24 ancore) | **−0,206** [−0,26, −0,16], >0 in 0% | −0,066, >0 in 12% |
| dSharpe hold-out | **−0,169** [−0,32, −0,15], **>0 in 0%** | −0,169, 0% |
| dCAGR | −2,27 pp, 0% | −1,32 pp, 4% |
| null del de-levering | Sh 3g 1,23 vs 2g×0,974 **1,44** → de-levering | 1,54 vs 1,53 → pari |
| per anno (dSh) | 2022 −0,55 · 2023 −0,66 · **2024 +0,54** · 2025 −0,44 · 2026 −0,25 → **1/5** | |
| corr(gamba XRP, book) | +0,35 / +0,37 (SOL 0,40-0,56) | |
**XRP DILUISCE** — lo stesso −0,17 di hold-out di SOL, in 0/24 ancore, con **un anno buono** (2024).
TP01 XRP da solo: Sharpe −0,01, hold-out −0,49; SKH01 XRP maxDD 40,1% (il criterio di selezione di
SKH01-V2-DD, maxDD<30%, fallisce anche qui, come SOL). Eseguibilita' non e' il vincolo (min 1 XRP =
$1,42). Non misurato: **SKH01 solo-short su una terza moneta** (la domanda riguarda i crolli, dove
paga lo short) — ma il filtro di direzione e' una famiglia nuova (M20) e il 5m di XRP e' peggio di
SOL: il muro e' lo stesso. **Nessuna proposta.** Il dato vive in `data/raw/alt_xrp_*` (namespace di
ricerca, non rinfrescato dal cron, `load_data("XRP")` fallisce; `tests/test_sol_leg.py` lo presidia
via `DERIBIT_INSTR`). La cache del riferimento in `data/_cache` e' gitignored e fuori backup: la
certificazione si riproduce **con rete** (N11 vale con rete).
## 4. Cosa cambia una decisione — niente; cosa si registra
| voce | prima | oggi |
|---|---|---|
| il libro nei crolli | "immune, beta 0,076" | perde nel GIORNO (−0,48%/g, 18% positivi), immune-o-positivo per finestra, **> 0 in 8/8 dal 2022** per SKH01 short marcato all'uscita; beta 0,0769 riprodotto |
| f di stress delle opzioni | non misurato ("serve un crash catturato") | crollo catturato **a vol bassa, sotto il gate, inverse**: f_net 0,74 = rally; f_mtm 1,08; ali 1,92×. **Condizione non soddisfatta nella sostanza** |
| peso SKH01 | gate fallisce (26/07) | invariato: la condizione che fallisce e' in-sample; cache segnali ferma al 25/07 |
| XRP | mai misurato | diluisce come SOL (−0,169 hold-out, 0/24) |
| universo Deribit | 14/19 (23/08) | 13/19 (APT inactive); liquidi solo BTC/ETH/XRP/SOL; BTCDVOL future non negoziabile; PAXG non misurato |
| debito nuovo | — | `cblib.spot_series` + `asof` guarda un'ora avanti (feed 1h etichettato all'apertura) |
**Cosa riaprirebbe qualcosa:** (a) un secondo crollo catturato **a IV-rank > 0,30** (VRP01 a
mercato), sulla famiglia USDC-lineare, per il f di stress; (b) `weights_tilt_null` che passa — si
rigenera `skh_sigtab` e si rilancia `r0726_reeval_live_weight`, non si ridiscute; (c) per le
"altre monete", un meccanismo che non sia TP01/SKH01 congelati, o 3 anni di dato 5m liquido.
## 5. Errori catturati in sessione (prima della revisione)
- Percorso ora per ora in un ciclo `groupby` per ancora: >10 minuti; riscritto su una tabella
ora×strumento (`Quote`), ~3 minuti.
- BTC "in geometria" senza guardia: la griglia rada faceva scegliere a `pick_legs` una δ−0,02 al
posto della δ−0,10 (K72000/55000). Guardia `TOL_D` (±0,08/±0,06), scritta dopo aver visto BTC ma
che per ETH scarta 2/288 e non cambia nessun verdetto (verificato dal revisore).
- Coinbase restituiva un batch vuoto per XRP 2022 e il ciclo si fermava: certificazione vuota.
Riscritto con avanzamento su batch vuoto e riferimento Bitstamp dove Coinbase manca.
## 6. Revisione (`fable`, agente fresco) — 21 segnalazioni, tutte verificate
Applicate: #1 artefatto del MTM (taglio alla larghezza; IV 108 e' a T→0) · #2 f_exp tautologico
(quota stampata, N_eff = strutture) · #3 "101%" = perdita massima + fee · #4 `asof` un'ora avanti
(`spot_causale`; debito su `cblib`) · #5 famiglia inverse dichiarata · #8 "durante" bimodale (p10/p90,
quota che perde) · #9 ordine delle classi (immune → POSITIVO → PERDE, tre affermazioni nel verdetto)
· #10 peggior giorno = crollo con TP01 0,505, docstring (c) corretto · #11 beta sulla finestra di §46
(0,0769, tolleranza 0,01) · #12 scomposizione esatta con assert · #13 lente "marcata all'uscita"
dichiarata · #14 confronto bit-exact con `_skyhook_returns` · #15 cache `skh_sigtab` ferma al 25/07
· #16 1,92 e' un punto nuovo, non "gia' noto" · #17 il crollo e' sotto la mediana del DVOL: verdetto
riscritto · #18-19 BTCDVOL future e PAXG registrati · #21 APT inactive, cache con rete.
Non applicate: nessuna. Il revisore ha trovato **due conclusioni che non reggevano** (il 2,6× e
l'"8/12 guadagna") e le ha smontate con numeri riprodotti: la regola *"chi rivede non e' chi ha
scritto"* ha pagato in una sessione.
**Test (Opus, agente separato): `tests/test_r0909_*.py`, 92 test, suite intera 1008/1008 verdi**
(erano 916). Coprono: ordine delle classi e riproduzione verbatim dei verdetti, `spot_causale` col
controllo positivo sul difetto che ripara, taglio alla larghezza, geometria `in_geom` derivata da
`VRP_CFG`, regola L-PULITA (anno pulito isolato ignorato), `_fetch_1h` su batch vuoti, P1 per
identita' (`W_TP01`, `XS_UNIVERSE`), zero rete e zero scritture in `data/`. **Due difetti trovati da
Opus nel verdetto di XRP, corretti:** un campione vuoto stampava "DILUISCE" (ora "NON MISURABILE",
P5) e il criterio per anno era cablato al 2024 invece che derivato dall'anno della lente (P1).
@@ -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,216 @@
# I cinque punti della revisione del 09/09 — decisi e fatti
**Data:** 2026-09-10 (sessione 12:08Z → 13:10Z; i timestamp qui dentro sono letti, non ricordati)
**Origine:** la prima revisione settimanale (`docs/revisioni/2026-09-09.md`, revisore `fable`) e il
giornale del 09/09. Ogni punto e' stato spiegato all'operatore con pro e contro e **deciso da lui**
(§3-style: decisioni con l'informazione completa). Issue sul Gitea di casa #2-#6, chiuse con la
soluzione scritta (regola globale del 10/09).
## Le decisioni, in una tabella
| # | punto | decisione dell'operatore | issue |
|---|---|---|---|
| 1 | `usde_watch` contava 400 USDE come reward (07/09) | ripara ora + issue | #2 bug |
| 2 | cuscino USDC senza sorvegliante (§5.17) | **equity USDC, riconverte da solo** | #3 sviluppo |
| 3 | versamento 04/09 solo rilevato; piano €5.000 «in attesa» | **e' l'INTERO versamento previsto**: dichiarato, piano chiuso | #6 sviluppo |
| 4 | SCALA-01 A2: da quando contano i 180 giorni | **dal 01/09** (sorvegliante attivo) → gate non prima del **2027-02-28** | #5 bug |
| 5 | PREVDAY-01 «senza data» | **tieni 21/06/2027, cabla kill e veto, correggi §4** | #4 bug |
Sul punto 5 la domanda giusta e' stata «ricorda dove dobbiamo arrivare (50 €/g)»: PREVDAY
de-luckato al 15% vale +0,10 di Sharpe sul libro e la somma di *tutti* i lead dell'ondata di agosto
vale +0,036 €/giorno. Verso i 50 €/g il candidato vale centesimi in qualunque esito del gate:
non si spende una sessione di ricerca per anticipare (a)+(b), e non si butta l'unica gamba short
scorrelata oltre a SKH01 per risparmiare un cron. Costo scelto: mezza giornata, una volta.
## Punto 1 — il lettore dei trade spot aveva DUE limiti, non uno
Il primo era noto dalla revisione: `trade_history(limit=50)` con 54 ordini. Il secondo l'ho
trovato sondando il gateway per ripararlo: `get_user_trades_by_instrument` chiamato senza
`historical` restituisce **solo le ultime ~24 ore** — il fill BTC dell'08/09 06:47Z e' invisibile
alle 12:55Z del 10/09, quello del 09/09 21:47Z si vede. Il cron gira ogni 24h alle 12:35: sta
dentro la finestra per pochi secondi. Un giro saltato avrebbe reso invisibili i trade fra 48h e
24h fa, e il delta sarebbe stato attribuito al reward *esattamente come il 07/09*, con un
meccanismo diverso.
Ora `trade_copertura()` dichiara **non leggibile** (somma `None`, e `analizza` non attribuisce:
P12) in entrambi i casi — lista lunga quanto il limite (troncata) o ultima lettura oltre 24h+5min —
e il record porta `trades_motivo` (P4: il perche'). `TRADE_LIMIT = 1000` e' il `count` massimo di
Deribit. Controllo positivo nei test: 54 ordini si sommano tutti.
La riga del 07/09 in `data/live/usde_watch.jsonl` e' **corretta in loco** con campo `correzione`
(originale conservato dentro, copia del file in `usde_watch.jsonl.pre_fix_20260910`): 54 ordini
= 8 + 30 + 16 dal registro `usde_convert.jsonl`, 2.434 USDE, reward 0,080934. Il tasso non
cambia — la riga era gia' esclusa per `con_trade` — e `rendimento()` oggi da' **3,94% su 14
finestre, 12 pagate**.
## Punto 2 — `cuscino_watch`: la grandezza che nessuno guardava
Il cuscino richiesto e' `2 × 0,5 × 0,30 × equity_tot` = 30% dell'equity, **derivato** da
config e `book.WEIGHT`. La formula e' passata da `usde_convert.py` a `src/live/usde.py`, e
`usde_convert` la importa da li': due lettori, una formula (P1 — cinque occorrenze nel
progetto di sorveglianti puntati su un bersaglio ridichiarato).
| stato | criterio | azione |
|---|---|---|
| OK | slack ≥ 10% del cuscino | niente |
| PREAVVISO | 0 ≤ slack < 10% | ⚠️ alla transizione |
| SCOPERTO | slack < 0 | 🚨 + `usde_convert --quota 0,64 --esegui` (cuscino + 20% di margine, alzato dal 10% in revisione) |
| BLIND | conto non leggibile | ⚠️ alla transizione |
Guardie della riconversione: `execution_enabled` del libro (un solo interruttore), niente vendita
sotto `depeg_warn` (li' decide l'operatore, e l'allarme lo dice), un tentativo ogni 6h, le guardie
proprie di `usde_convert` (banda di prezzo, book leggibile, tetto HARD) ereditate lanciando lo
script vero (P15). Il marcatore «gia' detto» e' l'esito di `notify`: un invio fallito non consuma
la transizione (lezione del debito §5.2). Cron `:53` = dopo il giro del book (`:47`), fuori dal
minuto tondo. `monitor_health` lo sorveglia (max 3h).
📌 **Primo giro a secco (13:00Z): gia' PREAVVISO.** USDC equity $1.361,23 contro $1.335,26
richiesti: **slack +$25,97**. Il 09/09 il revisore aveva stimato +$41/+$59; la marcatura del
libro dal picco del 06/09 ne aveva consumati altri 15-30 senza che nessuno lo vedesse. E' il
caso d'uso del sorvegliante, arrivato prima del sorvegliante.
## Punto 3 — il versamento del 04/09, dal balance e non dall'equity
Il salto di equity 13:47→14:47Z diceva +$2.410,14 col mercato dell'ora dentro. Il **balance
USDC** di `balance_watch` (cambia solo per P&L realizzato, fee, funding e movimenti) dice
1.417,73455581 → 3.832,41432481 = **+$2.414,68**, nessun fill nella finestra, funding su $541
di nozionale ~$0,01. Dichiarato con banda $0,05. Il report ora lo mostra come dichiarato e
il TWR passa a **+6,66%** (era +7,22% stamattina, +7,83% il 06/09): la coda −0,63% dal 04/09 e'
marcatura delle due long, trading da arming **+$13,82**.
L'operatore ha detto che era **l'intero versamento previsto**: la voce «in attesa di importo e
data» di §1 e' chiusa. Resta la lezione di §2: per sei giorni il movimento e' stato solo
rilevato, la regola diceva il giorno stesso.
## Punto 4 — SCALA-01: la data era il punto 5 della SPEC scambiato per il gate
«Non prima del 2026-10-01» erano i 30 giorni di sorvegliante del punto 5 (§7 della SPEC); A2
chiede **≥180 giorni** col criterio passato ogni giorno, e il criterio lo misura solo
`scale_watch`, attivo dal 01/09. Le tre letture possibili, con pro e contro, sono nel diario di
sessione; l'operatore ha scelto **dal 01/09 → 2027-02-28**. Contare dall'arming avrebbe dato
un criterio ricostruito a posteriori, che non prova nulla sul sorvegliante; ridurre A2 a 30 giorni
avrebbe allentato un gate senza un costo d'attesa che lo giustifichi (il gradino 1,25x vale meno
di €100/mese di versamento).
Riconciliata anche la data d'arming: **20/06 = TP01 da solo** (commit `4650aa7`), **23/06 = il
BOOK** (commit `db738bc`, prima lettura di equity 23/06 22:00Z). CLAUDE.md §0 diceva solo la
prima, `config/live.json` e il TWR usano la seconda: erano due eventi, non un errore.
## Punto 5 — PREVDAY-01 aveva la data; non l'aveva la tabella
`RESULTS-0822` §54: decisione **2027-06-21**, kill, veto. La tabella §4 diceva «scritto 23/08 ·
10 condizioni», e il revisore ha letto quella. Ora la riga ha data, kill, veto e stato (5/7:
(a) e (b) sono strutturali, nessun forward le cambia).
Cablato in `paper_prevday.py` (prima esisteva solo nel testo, a differenza di `paper_dvolspread`):
| misura | valore al 10/09 |
|---|---|
| Sharpe forward **giornaliero** (lente del kill; l'oraria +1,19 non si cita, §54) | **+0,95** su 81 giorni, 81 attivi |
| kill (< −0,50 su ≥180 g attivi) | **NON MATURO**, leggibile dal ~18/12/2026 |
| barre ricostruibili dal feed di oggi | **1.942/1.943** (99,9%); l'unica divergente e' il 26/08 00:00, il giorno del fix di `advance()` |
| veto | ok |
⚠️ **Errore mio, catturato prima di committare:** la prima stesura del veto leggeva «divergenze
non crescenti» come `tasso_recente > tasso_totale`, e con UNA barra divergente negli ultimi 30
giorni (0,03/g contro 0,01/g) dichiarava VETO. Una guardia piu' stretta del contratto produce
allarmi che si impara a ignorare (P14): ora la crescita richiede **≥5 barre recenti E tasso
doppio**, dichiarato nel codice e provato nel test con i numeri della serie vera.
La ricostruzione al 99,9% contro il 95,6% di §54: la misura del 23/08 era stata fatta *prima*
del fix di `advance()` del 26/08 (le 67 barre divergenti erano «all'ora del cron», cioe' barre
parziali); oggi il feed rivisto e la serie coincidono tranne quella barra.
## Cose trovate per strada
- Due timestamp «di comodo» scritti a mano (13:05Z e 13:07Z) invece dell'ora letta (12:57:43Z e
13:04:09Z): corretti subito. E' esattamente la regola globale «la data si LEGGE» — vale anche per
chi la scrive nel campo `correzione`.
- `issue --json` va **prima** del sottocomando; il percorso nella regola globale era
`/opt/docker/AI-OS` invece di `/opt/AI-OS` (corretto la mattina, nel test dello strumento).
## Verifica
`uv run pytest`: **1060 passati** (erano 1031). Nuovi: `test_cuscino_watch.py` (14),
`test_paper_prevday_gate.py` (8), +5 in `test_usde_watch.py`. Revisione del diff affidata a
`fable` (agente fresco): esito e correzioni applicate in coda a questo diario.
## Revisione `fable` (13:10-13:18Z): 15 segnalazioni, 4 medie — tutte verificate, 12 applicate
| # | segnalazione | verifica | esito |
|---|---|---|---|
| 1 | `puo_riconvertire` contava anche i NON tentativi (`tentata=False`): un giro `--secco` o un prezzo illeggibile bloccava la riconversione vera per 6h | riprodotta | filtro su `tentata` + test |
| 2 | il tetto del venue in `usde_convert.piano` bloccava anche la **vendita**: con `venue_cap_frac` rimesso in config la riconversione automatica sarebbe stata morta | riprodotta (`side=sell`, `ok=False`) | vale solo in acquisto |
| 3 | `MARGINE_RIPRISTINO` 10% == `PREAVVISO_FRAC` 10%: dopo ogni 🚨 un ⚠️ per costruzione (P14) | riprodotta col floor di 1 USDE | margine **20%** → quota **0,64**, test |
| 4 | `--secco` scriveva nel registro vivo (veicolo del punto 1, e maschera un cron fermo) | riprodotta (riga 13:00:15Z) | non scrive; riga tolta |
| 5 | due bersagli di quota: `quota_target` 0,70 in config contro 0,64 del ripristino; il ripristino e' un **ratchet verso il basso** | letto | dichiarato in CLAUDE.md §5.17 e nel docstring: decisione dell'operatore (N9) |
| 7 | buco di ≤5 min dentro la tolleranza della finestra 24h | letto | dichiarato (D5) + fonte Deribit documentata |
| 8 | `giorni_attivi` legge posizioni REAL, il kill e' MODELED | letto | dichiarato nel docstring (81/81 oggi) |
| 10 | il test del cron leggeva un commento nello `.sh`, non la crontab | letto | legge `crontab -l` come `test_book_cadenza` |
| 11 | `prevday_target` patchato senza `monkeypatch` | letto | `monkeypatch` |
| 13 | «QUATTRO movimenti» in §2: sono TRE, in quattro tratti | letto | corretto |
| 14 | «5/7» contro «8/10» di §54: base diversa non dichiarata | letto | dichiarata |
| 6, 9, 12, 15 | verifiche che reggono (`sys.executable`, `cwd`, `piano()` in SCOPERTO, ricostruzione ≡ `advance`) | — | — |
Non applicato: il lock fra cron e conversione a mano (DUBBIO 2) — dichiarato come limite nel
docstring. Il DUBBIO 1 (ratchet) e' la domanda aperta per l'operatore.
## Coda (13:55Z →): il riacquisto automatico — issue #7
Alla domanda aperta (ratchet verso il basso) l'operatore ha risposto «crea una strategia per
riportarla a quota in autonomia». La risposta ingenua — ricomprare fino a `quota_target` 0,70 —
e' sbagliata per costruzione: a 0,70 lo slack e' zero, la prima ora in perdita vende, il
riacquisto ricompra, e cosi' via a 6 bps al giro. Serve **isteresi con un bersaglio unico**:
| slack / cuscino | stato | azione |
|---|---|---|
| < 0 | SCOPERTO | 🚨 vende USDE → 0,20 |
| 0 – 0,10 | PREAVVISO | ⚠️ |
| 0,10 – 0,40 | OK | — |
| > 0,40 | **ECCEDENTE** | 📌 compra USDE → 0,20 |
Bersaglio 0,20 ⇒ quota **0,64**. Per oscillare lo slack deve muoversi di ±0,20×cuscino = ±6%
dell'equity, cioe' **±8,6% di equity USDC (~$380)** perche' slack = 0,7·USDC − 0,3·USDE (la prima
stesura diceva ±6%/$270: corretta in revisione): a leva 0,26x sono ~33% di mercato. Un bonifico alza lo slack di
0,7×importo: sopra ~9% dell'equity (~$380) il riacquisto scatta da solo — la regola di §1
«dopo un bonifico si rilancia `usde_convert`» non dipende piu' da qualcuno che se ne ricordi.
Le tre frazioni vivono in `config/live.json` (`usde.bande_cuscino` verifica 0 < preavviso <
margine < riacquisto), `quota_target` 0,70 e' stato tolto. Soglie dichiarate, non ottimizzate
(M8): il costo di un giro e' ~$0,2, la frequenza attesa qualche giro al mese nei tratti mossi.
📌 **Trovato al primo giro vero del cron (13:53Z): l'allerta PREAVVISO non e' partita.** Il token
era valido (`getMe` ok); il testo conteneva «(< 10% del cuscino)» e Telegram in `parse_mode=HTML`
rifiuta un `<` nudo. Testi riscritti senza `<`, `allerta_errore` registrato nel record (P3),
test che legge il sorgente delle `notify`. Un sorvegliante nuovo che non riesce a parlare al
primo giro e' il caso di P2: si valida sul segnale E sul trasporto.
Stato al giro a secco delle 14:0xZ: quota 69,3%, slack +$30 = PREAVVISO. Il piano a 0,64 e'
valido (SELL 237 USDE, slack dopo +$267): il sorvegliante lo esegue alla prima ora sotto zero;
allinearlo subito e' una riga a mano.
### Seconda revisione `fable` (14:04Z): 10 segnalazioni, 1 ALTA, 3 MEDIE — applicate 9
| # | segnalazione | esito |
|---|---|---|
| 1 ALTA | il `<` arrivava ancora a Telegram dal campo `motivo` («(< 6h)») dentro l'allerta 🚨: il 🚨 piu' importante perso fino a 4h | escape nel **sink** `notifier.notify` (`html.escape` su titolo e valori; nessuno dei 45 chiamanti passa tag: grep) + motivi senza `<` |
| 2 MEDIA | il test sul sorgente era parziale (codice morto, cieco ai valori interpolati) | sostituito: cattura la POST e verifica `&lt;` |
| 3 MEDIA | ECCEDENTE con guasto persistente = «acquisto FALLITA» ogni 6h per sempre | da ECCEDENTE dopo un fallimento si riprova ogni 24h; da SCOPERTO resta 6h |
| 4 MEDIA | bande fuori ordine ⇒ traceback ingoiato dal cron, nessun record, «fermo» invece di «rotto» | `giudica` ⇒ BLIND con motivo; record e allerta scritti |
| 5-7 | CLAUDE.md §1 diceva ancora «nessuno sorveglia»; §5.17 «tre stati»; `_nota_usde` con `quota_target` | corretti |
| 8 | «±6% di equity USDC» sbagliato di 0,7: slack = 0,7·USDC − 0,3·USDE ⇒ ±8,6% (~$380) | corretto in 4 posti |
| 9 | `TOLL` ridichiarato nel test | importato da `usde_convert` |
| 10 | esempi `--quota 0.70` nel docstring di `usde_convert` | 0,64 |
Verifiche numeriche del revisore (loop su `piano()` con passo 1 USDE, TOLL, min order): bonifico
+$2.000 → 11 acquisti → OK, slack +$387; perdita −$50 → 3 vendite → OK, +$264; conto tutto USDC →
20 acquisti → OK. **Nessuna sequenza vendita→acquisto→vendita.** Non applicato: `min(q*, cap)` con
il tetto del venue (dubbio 3: un conto al tetto resta ECCEDENTE e ora allerta una volta al giorno).
Dubbio 4, che vale una riga in §5.17: **0,64 e' una derivazione dell'autore, non la quota decisa
dall'operatore il 30/08 (0,70)**; ~$10/anno di resa di differenza; da confermare.
### Allineamento eseguito (14:16:38Z, ordine dell'operatore «allinea subito a 0.64, esegui»)
`usde_convert --quota 0.64 --esegui`: SELL 100 + 100 + 38 USDE a 1,0000 medio (limite 0,9995,
fill al book), quota 69,4% → **64,02%**, conto USDC $1.602,79 + USDE 2.851,95 = $4.454,74.
Slack dopo **+$266** su $1.336 di cuscino: `cuscino_watch --secco` dice **OK** (era PREAVVISO a
+$29). Costo: ~$0,1 di spread, fee zero. Registro in `data/live/usde_convert.jsonl`.
+52
View File
@@ -0,0 +1,52 @@
# Risk-off ±2h intorno a FOMC e CPI su TP01 — REFUTED, e il segno e' l'opposto
**Data:** 2026-09-10 14:52-14:55Z · **Script:** `scripts/research/r0910_macro_riskoff.py` (N11: il
verdetto lo calcola lo script) · **Issue:** #9 · **Origine:** domanda dell'operatore da un articolo su
EA forex a griglia con «agente fondamentale» che approva/rifiuta i segnali intorno ai dati macro.
## Cosa si e' misurato
TP01 canonico (posizione decisa alla chiusura giornaliera, tenuta 24 barre 1h), 2019-03 → 2026-09,
65.676 ore. Variante RISK-OFF: esposizione zero nelle barre che intersecano [t−2h, t+2h] intorno a
**61 dichiarazioni FOMC** (14:00 ET, piu' le due d'emergenza di marzo 2020, notation vote esclusi) e
**88 release CPI** (08:30 ET), con fee di chiusura e riapertura. Calendario **letto dal web** (Fed e
BLS), non ricordato; l'archivio BLS raggruppa per anno di riferimento e le release di gennaio/febbraio
vanno spostate di +1 anno (verificato sulla schedule 2026). 684 ore in finestra = **1,04%** del tempo.
Verdetto pre-registrato prima del primo numero: LEAD solo se (a) ΔSharpe con fee > 0, (b) Δ al ≥95°
percentile del null location-matched (stessi conteggi per anno, stesse ore del giorno, giorni a caso,
1.000 estrazioni — M18), (c) Δ > 0 in ≥70% degli anni (M9).
## Numeri
| | Sharpe giornaliero | drift/anno |
|---|---|---|
| TP01 base | 1,302 | +15,73% |
| risk-off con fee | 1,205 | +14,49% |
| risk-off senza fee | 1,228 | +14,77% |
| **Δ** | **−0,097** | **−1,24%** ≈ **−0,14 €/giorno** a $4.455 |
Per tipo: FOMC ΔSh −0,012 (P&L rinunciato +0,79%), **CPI −0,084 (+6,36%)**. Per anno: Δ negativo in
6/8, positivo solo 2024 e 2026 (+0,02/+0,03).
Null location-matched: ΔSh mediana −0,033 [p05 −0,095, p95 +0,027]. **Il Δ vero sta al 4,2°
percentile**: le finestre vere sono fra le PEGGIORI da appiattire, cioe' fra le migliori in cui
essere esposti. Dentro la finestra TP01 fa **+1,05 bp/ora contro +0,17 fuori** (t = 1,57 su 684 ore).
(a) FAIL · (b) FAIL · (c) 2/8 FAIL ⇒ **REFUTED**.
## Lettura
1. **Il risk-off toglie P&L, non rischio.** TP01 e' long-flat vol-targeted: nelle ore dei dati macro
il trend che sta cavalcando accelera piu' spesso di quanto si rovesci, e l'1% del tempo in finestra
porta il 7% del guadagno cumulato. E' la stessa lezione del weekend (§3, 17/07: il 38% del gross
nel 31% del tempo): **la coda del trend vive nelle ore «pericolose»**.
2. **Il segno opposto non e' un lead.** «Esporsi DI PIU' intorno ai dati» sarebbe un'ipotesi nuova
(M12), con t = 1,57 e 684 ore: sotto qualunque MDE, e la selezione sull'esito la vieta. Non si apre.
3. Il gate «fondamentale» dell'articolo, applicato al nostro libro, costerebbe **−1,24%/anno di drift**
— la stessa taglia del funding non modellato (−2,16%), ma con segno scelto da chi lo propone.
4. Coerente con `macro-regime-gate` (29/06, ridondante col trend) e con la conclusione di §3: **ogni
risk-off parte REFUTED** e questo lo e' rimasto.
Non modellato (dichiarato): funding (identico nelle due varianti salvo l'1% delle ore); slippage
della finestra (rende il risk-off *piu'* costoso, non meno). Costo: 3 minuti di calcolo, 0 ordini.
+60
View File
@@ -0,0 +1,60 @@
# Giornale di bordo — 2026-09-02
*Scritto 2026-09-03T00:36:49+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $77,312.50 | -0.15% | -2.17% | +21.78% | 42.9% | ↑ ↑ ↑ (3/3 su) | 37.2 (16° pctl 1a) |
| **ETH** | $2,391.55 | -1.11% | -4.63% | +28.63% | 68.5% | ↑ ↑ ↑ (3/3 su) | 50.9 (17° pctl 1a) |
## Libro
Equity **$2,045.82** · nozionale lordo $560 · leva lorda 0.27x · barra dati 2026-09-02 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.454 | flat | $+348 | $+347 | HOLD (a target) |
| **ETH** | +0.276 | flat | $+212 | $+213 | HOLD (a target) |
## P&L
- giorno: **$-4.95** di equity (24 letture)
- realizzato: -0.35 netto su 1 round-trip chiusi · 2 fill · fee 0.0045
- cumulato dall'arming: **$+1,447.76** di equity — di cui $+1,399.39 versati/prelevati -> trading **$+48.37** · TWR **+10.47%**
## 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 $+347 (TP01 +0.454, SKH01 flat) e ETH $+213 (TP01 +0.276, SKH01 flat).
- `[leva]` Leva lorda **0.27x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.73x.
- `[pnl]` Equity $-4.95 con 2 fill e 1 round-trip chiusi (realizzato $-0.35 netto).
- `[drawdown]` 📌 Equity **-1.34%** sotto il picco ($2,073.59). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.2 contro realizzata 30g 42.9 (sotto); DVOL al 16° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.9 contro realizzata 30g 68.5 (sotto); DVOL al 17° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **71 giorni, 30 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-03T00:37:49+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto nuovo e' l'equity a $2,045.82: sotto ogni chiusura registrata dopo il versamento del 25/08, dove il minimo era $2,047.39 del 30/08. Non viene dal trading — realizzato $-0.35 su un round-trip, fee 0.0045, due fill — ma dal prezzo su due posizioni tenute a target per tutte le 24 letture.
Il picco da cui si misura il -1.34% ($2,073.59) non compare fra le chiusure giornaliere: e' una lettura oraria, quindi quel drawdown vive su una griglia piu' fine della tabella dei giorni.
La tensione del giorno e' fra le scale: TSMOM 3/3 su su entrambi gli asset, ma 1g e 7g negativi (BTC -2.17%, ETH -4.63% a 7g) contro 30g ancora +21.78% e +28.63%. Il libro resta long perche' la gamba lunga tiene; una lettura altrettanto compatibile e' che i 30g stiano semplicemente restituendo il rialzo accumulato. Quale delle due, non e' misurabile qui.
Insolito il pacchetto volatilita': implicita sotto la realizzata su entrambi (37.2 contro 42.9, 50.9 contro 68.5) con DVOL al 16° e 17° percentile dell'anno, mentre il gate di VRP01 legge l'IV-rank (0.06 e 0.12) e resta fermo — guarda una cosa diversa da quello scarto.
Meriterebbe un occhio umano il perche' il $-4.95 arrivi con salute perfetta, 24/24 giri e feed a 0 min. Il campione resta 71 giorni e 30 round-trip, con $+48.37 di trading sui $+1,447.76 cumulati.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-03
*Scritto 2026-09-04T00:37:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $81,286.00 | +5.14% | +1.27% | +26.90% | 45.5% | ↑ ↑ ↑ (3/3 su) | 39.8 (38° pctl 1a) |
| **ETH** | $2,507.90 | +4.87% | -0.15% | +34.20% | 69.8% | ↑ ↑ ↑ (3/3 su) | 53.3 (27° pctl 1a) |
## Libro
Equity **$2,074.42** · nozionale lordo $562 · leva lorda 0.27x · barra dati 2026-09-03 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.453 | flat | $+352 | $+349 | HOLD (a target) |
| **ETH** | +0.274 | flat | $+213 | $+213 | HOLD (a target) |
## P&L
- giorno: **$+28.60** di equity (24 letture)
- realizzato: -0.14 netto su 4 round-trip chiusi · 4 fill · fee 0.0095
- cumulato dall'arming: **$+1,476.36** di equity — di cui $+1,399.39 versati/prelevati -> trading **$+76.97** · TWR **+12.02%**
## 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 $+349 (TP01 +0.453, SKH01 flat) e ETH $+213 (TP01 +0.274, SKH01 flat).
- `[leva]` Leva lorda **0.27x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.73x.
- `[pnl]` Equity $+28.60 con 4 fill e 4 round-trip chiusi (realizzato $-0.14 netto).
- `[vol]` BTC: implicita 39.8 contro realizzata 30g 45.5 (sotto); DVOL al 38° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.3 contro realizzata 30g 69.8 (sotto); DVOL al 27° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +5.14%, **2.2 deviazioni** giornaliere (sd implicita dalla RV30 = 2.38%).
- `[evidenza]` 📌 Campione a oggi: **72 giorni, 34 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-04T00:38:30+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto nuovo non e' il segno, e' la taglia: $+28.60 e un'equity a $2,074.42, la piu' alta delle dieci sedute mostrate, dopo giorni in cui il delta si e' mosso fra −17.87 e +12.06. Arriva da un BTC a +5.14%, 2.2 deviazioni, mentre il libro era gia' a target su entrambe le gambe: con nozionale lordo $562 e leva 0.27x, l'aumento e' rivalutazione di una posizione ferma, non l'esito di una decisione presa oggi. Il realizzato lo conferma — −0.14 netto su 4 round-trip, rumore di fee (0.0095) — e le posizioni restano $+349 e $+213, in linea con le sedute precedenti.
Insolito il quadro vol: entrambe le implicite sotto la realizzata 30g (39.8 contro 45.5, 53.3 contro 69.8), coi DVOL al 38° e 27° percentile dell'anno, in una giornata a due deviazioni. Non misurabile qui se sia salita la realizzata oggi o se l'implicita non abbia seguito.
Lettura alternativa, ugualmente compatibile: BTC fa +5.14% in un giorno ma +1.27% in sette, ed ETH −0.15%; buona parte del guadagno e' recupero dentro la settimana, non nuova direzione, benche' TSMOM sia 3/3 su su entrambi.
Un solo giorno pesa molto su un trading da $+76.97 in 72 giorni e 34 round-trip. Varrebbe un controllo umano sui 4 fill di una giornata chiusa a target: dovrebbero essere ri-taglie.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-04
*Scritto 2026-09-05T00:36:37+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $79,660.50 | -2.00% | +2.33% | +23.30% | 46.6% | ↑ ↑ ↑ (3/3 su) | 38.0 (24° pctl 1a) |
| **ETH** | $2,457.05 | -2.03% | +0.56% | +28.81% | 70.5% | ↑ ↑ ↑ (3/3 su) | 51.1 (18° pctl 1a) |
## Libro
Equity **$4,483.73** · nozionale lordo $1,173 · leva lorda 0.26x · barra dati 2026-09-04 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.428 | flat | $+719 | $+717 | HOLD (a target) |
| **ETH** | +0.270 | flat | $+455 | $+456 | HOLD (a target) |
## P&L
- giorno: **$+2,409.31** di equity (24 letture) — di cui **$+2,410.14 movimenti di capitale** -> trading **$-0.83**
- realizzato: -0.02 netto su 2 round-trip chiusi · 5 fill · fee 0.2328
- cumulato dall'arming: **$+3,885.67** di equity — di cui $+3,809.53 versati/prelevati -> trading **$+76.14** · TWR **+11.70%**
## 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.*
- `[movimento]` 📌 **Movimento di capitale $+2,410.14** fra le 13:47 e le 14:47 UTC: salto +116.8% contro un massimo spiegabile dal mercato di ±0.6% al tetto di leva. Scorporato dal P&L di trading qui sotto.
- `[stato]` A mercato su BTC $+717 (TP01 +0.428, SKH01 flat) e ETH $+456 (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]` P&L di trading $-0.83 (equity $+2409.31 al lordo del movimento di capitale) con 5 fill e 2 round-trip chiusi (realizzato $-0.02 netto).
- `[vol]` BTC: implicita 38.0 contro realizzata 30g 46.6 (sotto); DVOL al 24° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.1 contro realizzata 30g 70.5 (sotto); DVOL al 18° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **73 giorni, 36 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-05T00:37:58+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto della giornata non è il mercato ma la cassa: l'equity passa da $2,074.42 a $4,483.73, e il salto di $+2,410.14 fra le 13:47 e le 14:47 UTC è il secondo dopo il delta +1,414.57 del 25/08. Il libro l'ha assorbito subito: le posizioni, ferme per dieci giorni fra $+344 e $+364 su BTC e fra $+205 e $+218 su ETH, oggi valgono $+717 e $+456, entrambe a target, a leva lorda 0.26x su un tetto di 1.00x. Nessuna scelta discrezionale: TP01 +0.428 e +0.270, SKH01 flat.
Il numero da maneggiare con cura è il $-0.83 di trading. BTC e ETH chiudono a -2.00% e -2.03% con $1,173 di nozionale lordo lungo: l'intervallo che contiene il versamento esce intero dal conteggio, quindi quel $-0.83 non copre la giornata piena. Il realizzato, $-0.02 netto su 2 round-trip e 0.2328 di fee, è compatibile sia con una giornata quasi neutra sia con una perdita di mercato finita fuori dal segmento — qui non si separa.
Insolito il quadro vol: implicita sotto la realizzata su entrambi (38.0 contro 46.6; 51.1 contro 70.5), DVOL al 24° e 18° percentile; VRP01 resta fermo per l'IV-rank (0.08 e 0.12), non per questo.
Salute piena, 24/24 giri e feed a 0 min; campione 73 giorni e 36 round-trip. Varrebbe che l'operatore confermi a mano importo e ora del versamento: la Nota è vuota.
## Nota
*(vuota — campo dell'operatore)*
+57
View File
@@ -0,0 +1,57 @@
# Giornale di bordo — 2026-09-05
*Scritto 2026-09-06T00:37:28+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $79,835.00 | +0.22% | +2.03% | +24.19% | 46.4% | ↑ ↑ ↑ (3/3 su) | 38.7 (29° pctl 1a) |
| **ETH** | $2,481.10 | +0.98% | +0.93% | +30.40% | 70.4% | ↑ ↑ ↑ (3/3 su) | 51.7 (21° pctl 1a) |
## Libro
Equity **$4,490.66** · nozionale lordo $1,161 · leva lorda 0.26x · barra dati 2026-09-05 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.420 | flat | $+708 | $+711 | HOLD (a target) |
| **ETH** | +0.268 | flat | $+451 | $+450 | HOLD (a target) |
## P&L
- giorno: **$+6.93** di equity (24 letture)
- realizzato: -0.16 netto su 3 round-trip chiusi · 3 fill · fee 0.0068
- cumulato dall'arming: **$+3,892.60** di equity — di cui $+3,809.53 versati/prelevati -> trading **$+83.07** · TWR **+11.87%**
## 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 $+711 (TP01 +0.420, SKH01 flat) e ETH $+450 (TP01 +0.268, 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 $+6.93 con 3 fill e 3 round-trip chiusi (realizzato $-0.16 netto).
- `[vol]` BTC: implicita 38.7 contro realizzata 30g 46.4 (sotto); DVOL al 29° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.7 contro realizzata 30g 70.4 (sotto); DVOL al 21° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **74 giorni, 39 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-06T00:38:25+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto della giornata non è di oggi: il 04/09 l'equity è passata da $2,074.42 a $4,483.73, +$2,409.31 in un giorno, e questa è la prima pagina intera scritta sul libro nuovo. La forma non è cambiata — leva lorda 0.26x, HOLD a target su entrambe le gambe, TP01 +0.420 e +0.268, SKH01 flat su tutte e due — ma le posizioni sì: $+711 e $+450 contro i $+347 e $+213 del 02/09. È cambiata la taglia, non il comportamento; e il $+6.93 di oggi va letto su una base doppia, dove la stessa cifra in dollari pesa meno di quanto pesava a fine agosto.
Il cumulato lo dice più chiaramente: $+3,892.60, di cui $+3,809.53 versati o prelevati e $+83.07 di trading in 74 giorni e 39 round-trip. La cifra citabile resta il TWR, +11.87%.
Insolito, e non spiegabile da questa pagina: implicita sotto realizzata su entrambi (38.7 contro 46.4, 51.7 contro 70.4) con DVOL al 29° e 21° percentile, mentre i 30 giorni segnano +24.19% e +30.40%. Lettura alternativa altrettanto compatibile col +$6.93: il mark-to-market di due long in una giornata salita (+0.22% e +0.98%), non un merito di selezione — con 3 fill e realizzato $-0.16 netto, qui non è distinguibile.
Un controllo umano lo merita il movimento del 04/09: che abbia la sua riga di giornale, non ricostruita dopo.
## Nota
*(vuota — campo dell'operatore)*
+57
View File
@@ -0,0 +1,57 @@
# Giornale di bordo — 2026-09-06
*Scritto 2026-09-07T00:36:43+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $80,376.00 | +0.68% | +3.44% | +23.88% | 46.4% | ↑ ↑ ↑ (3/3 su) | 39.3 (35° pctl 1a) |
| **ETH** | $2,515.10 | +1.37% | +4.04% | +31.44% | 70.4% | ↑ ↑ ↑ (3/3 su) | 53.2 (27° pctl 1a) |
## Libro
Equity **$4,502.93** · nozionale lordo $1,175 · leva lorda 0.26x · barra dati 2026-09-06 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.420 | flat | $+710 | $+717 | SELL $-7 |
| **ETH** | +0.268 | flat | $+453 | $+458 | HOLD (a target) |
## P&L
- giorno: **$+12.27** di equity (24 letture)
- realizzato: +0.13 netto su 1 round-trip chiusi · 1 fill · fee 0.0028
- cumulato dall'arming: **$+3,904.87** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+70.44** · TWR **+8.02%**
## 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 $+717 (TP01 +0.420, SKH01 flat) e ETH $+458 (TP01 +0.268, 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 $+12.27 con 1 fill e 1 round-trip chiusi (realizzato $+0.13 netto).
- `[vol]` BTC: implicita 39.3 contro realizzata 30g 46.4 (sotto); DVOL al 35° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.2 contro realizzata 30g 70.4 (sotto); DVOL al 27° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **75 giorni, 40 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-07T00:37:44+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto del giorno non e' il $+12.27: e' che dopo il salto del 04/09 il libro ha ripreso la sua forma abituale a una taglia nuova. Le posizioni sono cresciute con l'equity ($+717 e $+458 contro $+349 e $+213 del 03/09), non col segnale: TP01 sta a 0.420 e 0.268, parziale su entrambe le gambe, e la leva lorda resta 0.26x su un tetto di 1.00x. SKH01 e' flat su BTC e su ETH — il quarto del libro che fa breakout oggi non porta nulla, e da quanti giorni sia cosi' non e' misurabile qui.
L'attivita' e' scesa: 1 fill e 1 round-trip contro i 3-5 dei giorni scorsi, e l'unica azione e' un SELL $-7 su una posizione da $+717. Vale un'occhiata umana, senza allarme: le soglie d'ordine sono in dollari assoluti e l'equity ha cambiato scala.
Cumulato $+3,904.87, di cui $+3,834.43 versati; il trading vale $+70.44 su 75 giorni e 40 round-trip — abbastanza poco che un movimento di capitale scorporato male se lo mangerebbe intero.
Una lettura alternativa dello stesso dato: implicite sotto le realizzate su entrambi (39.3 contro 46.4, 53.2 contro 70.4) e DVOL al 35° e 27° percentile dopo trenta giorni a +23.88% e +31.44%. Le regole ci leggono il gate VRP01 chiuso; e' altrettanto compatibile un rialzo che il mercato delle opzioni non sta pagando.
## Nota
*(vuota — campo dell'operatore)*
+57
View File
@@ -0,0 +1,57 @@
# Giornale di bordo — 2026-09-07
*Scritto 2026-09-08T00:36:58+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $79,100.00 | -1.59% | +0.68% | +21.85% | 47.1% | ↑ ↑ ↑ (3/3 su) | 38.8 (31° pctl 1a) |
| **ETH** | $2,490.35 | -0.98% | +0.91% | +29.98% | 70.6% | ↑ ↑ ↑ (3/3 su) | 52.6 (25° pctl 1a) |
## Libro
Equity **$4,484.18** · nozionale lordo $1,156 · leva lorda 0.26x · barra dati 2026-09-07 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.420 | flat | $+707 | $+704 | HOLD (a target) |
| **ETH** | +0.268 | flat | $+451 | $+452 | HOLD (a target) |
## P&L
- giorno: **$-18.75** di equity (24 letture)
- realizzato: +0.10 netto su 1 round-trip chiusi · 3 fill · fee 0.0066
- cumulato dall'arming: **$+3,886.12** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+51.69** · TWR **+7.57%**
## 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.420, SKH01 flat) e ETH $+452 (TP01 +0.268, 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 $-18.75 con 3 fill e 1 round-trip chiusi (realizzato $+0.10 netto).
- `[vol]` BTC: implicita 38.8 contro realizzata 30g 47.1 (sotto); DVOL al 31° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.6 contro realizzata 30g 70.6 (sotto); DVOL al 25° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **76 giorni, 41 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-08T00:37:50+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
La giornata rompe tre chiusure positive di fila (+28.60, +6.93, +12.27) con $-18.75, la perdita giornaliera più grande della serie qui riportata. È quasi tutta valutazione, non attività: 3 fill, 1 round-trip, realizzato $+0.10 netto, con BTC -1.59% e ETH -0.98% e il libro long su entrambi. Il segno torna; la taglia non è verificabile qui, perché la pagina non scompone il P&L per asset.
Ciò che è cambiato davvero risale al salto del 04/09: le posizioni sono passate dai $+357 / $+214 di fine agosto ai $+704 / $+452 di oggi e ci restano, mentre la leva lorda resta 0.26x. A parità di rischio relativo la stessa percentuale di mercato si scrive con un numero di dollari più grande: le prossime giornate rosse sembreranno peggiori senza esserlo.
Insolito il quadro della volatilità: implicita sotto la realizzata su entrambi (38.8 contro 47.1; 52.6 contro 70.6) con DVOL al 31° e al 25° percentile di un anno. Il gate di VRP01 legge l'IV-rank (0.11 e 0.13) e resta chiuso, ma una lettura altrettanto compatibile è che il mercato tratti il movimento a 30 giorni (+21.85%, +29.98%) come già avvenuto.
TP01 porta segnali parziali (0.420, 0.268) e SKH01 è flat malgrado TSMOM 3/3 su: c'è tendenza, non breakout. Il cumulato $+3,886.12 è quasi tutto versato ($+3,834.43); resta $+51.69 di trading su 76 giorni e 41 round-trip, TWR +7.57% — campione che non separa edge e fortuna. Salute piena, 24/24 giri e feed a 0 min: niente da controllare a mano oggi.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-08
*Scritto 2026-09-09T00:36:48+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $78,470.50 | -0.80% | +1.34% | +20.97% | 47.3% | ↑ ↑ ↑ (3/3 su) | 39.9 (40° pctl 1a) |
| **ETH** | $2,485.45 | -0.20% | +2.77% | +30.16% | 70.6% | ↑ ↑ ↑ (3/3 su) | 53.9 (33° pctl 1a) |
## Libro
Equity **$4,477.46** · nozionale lordo $1,149 · leva lorda 0.26x · barra dati 2026-09-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.415 | flat | $+697 | $+698 | HOLD (a target) |
| **ETH** | +0.268 | flat | $+449 | $+451 | HOLD (a target) |
## P&L
- giorno: **$-6.72** di equity (24 letture)
- realizzato: +0.00 netto su 1 round-trip chiusi · 2 fill · fee 0.0055
- cumulato dall'arming: **$+3,879.40** di equity — di cui $+3,834.43 versati/prelevati -> trading **$+44.97** · TWR **+7.41%**
## 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 $+698 (TP01 +0.415, SKH01 flat) e ETH $+451 (TP01 +0.268, 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 $-6.72 con 2 fill e 1 round-trip chiusi (realizzato $+0.00 netto).
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($4,502.93). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 39.9 contro realizzata 30g 47.3 (sotto); DVOL al 40° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.9 contro realizzata 30g 70.6 (sotto); DVOL al 33° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **77 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-09T00:37:27+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Terzo giorno sotto il picco del 06/09 ($4,502.93), e secondo consecutivo in rosso dopo i $-18.75 del 07/09: oggi $-6.72. La differenza rispetto alla settimana precedente al versamento non e' il segno ma la scala — le posizioni sono passate da BTC $+347/ETH $+213 a BTC $+698/ETH $+451, quindi oscillazioni di equity dello stesso ordine in dollari valgono ora meno in percentuale. Con realizzato $+0.00 su 1 round-trip e HOLD a target su entrambe le gambe, la perdita del giorno e' rivalutazione delle posizioni aperte contro BTC -0.80% ed ETH -0.20%, non attivita' di trading.
L'elemento insolito e' altrove: TSMOM 3/3 su su entrambi gli asset, BTC +20.97% e ETH +30.16% a 30 giorni, e nonostante questo TP01 sta a +0.415 e +0.268 con SKH01 flat — leva lorda 0.26x su un tetto di 1.00x. Il libro e' poco investito dentro il trend che dovrebbe essere il suo caso favorevole; se questo sia disciplina di sizing o segnale debole non e' misurabile qui.
Lettura alternativa altrettanto compatibile: a 0.26x di esposizione il P&L giornaliero e' quasi interamente beta di mercato, e i 77 giorni / 42 round-trip non permettono di distinguerlo dall'edge.
Merita un'occhiata umana il round-trip chiuso con realizzato esattamente $+0.00 a fronte di fee 0.0055.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -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)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-10
*Scritto 2026-09-11T00:36:36+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $76,549.50 | -2.22% | -5.83% | +20.44% | 47.7% | ↑ ↑ ↑ (3/3 su) | 40.2 (43° pctl 1a) |
| **ETH** | $2,437.50 | -1.23% | -2.81% | +29.57% | 70.5% | ↑ ↑ ↑ (3/3 su) | 53.4 (30° pctl 1a) |
## Libro
Equity **$4,454.86** · nozionale lordo $1,147 · leva lorda 0.26x · barra dati 2026-09-10 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.419 | flat | $+700 | $+698 | HOLD (a target) |
| **ETH** | +0.269 | flat | $+449 | $+449 | HOLD (a target) |
## P&L
- giorno: **$-17.24** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 1 fill · fee 0.0027
- cumulato dall'arming: **$+3,856.80** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+17.83** · TWR **+6.76%**
## 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 $+698 (TP01 +0.419, SKH01 flat) e ETH $+449 (TP01 +0.269, 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 $-17.24 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto).
- `[drawdown]` 📌 Equity **-1.07%** 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 47.7 (sotto); DVOL al 43° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.4 contro realizzata 30g 70.5 (sotto); DVOL al 30° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **79 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-11T00:38:42+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
La giornata prolunga una sequenza iniziata prima. Dal picco di $4,502.93 del 2026-09-06 ogni giornata ha chiuso in perdita, e i $-17.24 di oggi sono vicini ai $-18.75 del 2026-09-07. Il confronto più istruttivo però è un altro. Il trading cumulato dall'arming, $+17.83, ha ormai la stessa taglia della perdita di una sola giornata come questa. È la forma concreta dell'avvertenza sul campione di 79 giorni.
Dal salto di equity del 2026-09-04 le posizioni in dollari sono rimaste quasi ferme: BTC fra $+717 e $+698, ETH fra $+458 e $+449. Nello stesso periodo BTC ha segnato -5.83% ed ETH -2.81% a 7 giorni, con i 30 giorni ancora a +20.44% e +29.57%. Il libro non ha quindi ridotto l'esposizione nel calo. Il quadro è compatibile con due letture: un ritracciamento dentro un trend che tutte le finestre TSMOM vedono intatto, oppure un cambio di regime che finestre da 30 giorni in su non possono ancora registrare. La pagina non le distingue.
Non è misurabile qui quanto dei $-17.24 venga dalla marcatura delle due long e quanto da funding o collaterale. Non lo è nemmeno l'asset dell'unico fill, visto che a fine giornata entrambe le gambe erano a target.
Merita un controllo umano la descrizione del libro nel prompt dell'analista. Indica un capitale molto inferiore all'equity di $4,454.86 e andrebbe aggiornata.
## Nota
*(vuota — campo dell'operatore)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-09-11
*Scritto 2026-09-12T00:36:53+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $77,220.50 | +0.88% | -3.06% | +21.72% | 47.6% | ↑ ↑ ↑ (3/3 su) | 36.8 (14° pctl 1a) |
| **ETH** | $2,516.70 | +3.25% | +2.43% | +34.00% | 70.8% | ↑ ↑ ↑ (3/3 su) | 51.8 (22° pctl 1a) |
## Libro
Equity **$4,466.16** · nozionale lordo $1,131 · leva lorda 0.25x · barra dati 2026-09-11 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.410 | flat | $+688 | $+687 | HOLD (a target) |
| **ETH** | +0.268 | flat | $+449 | $+444 | HOLD (a target) |
## P&L
- giorno: **$+11.30** di equity (24 letture)
- realizzato: +21.66 netto su 13 round-trip chiusi · 8 fill · fee 0.4046
- cumulato dall'arming: **$+3,868.10** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+29.13** · TWR **+7.03%**
## 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 $+687 (TP01 +0.410, SKH01 flat) e ETH $+444 (TP01 +0.268, SKH01 flat).
- `[leva]` Leva lorda **0.25x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.75x.
- `[pnl]` Equity $+11.30 con 8 fill e 13 round-trip chiusi (realizzato $+21.66 netto).
- `[drawdown]` 📌 Equity **-0.83%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.8 contro realizzata 30g 47.6 (sotto); DVOL al 14° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.8 contro realizzata 30g 70.8 (sotto); DVOL al 22° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **80 giorni, 55 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.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-12
*Scritto 2026-09-13T00:37:30+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $77,288.50 | +0.09% | -3.19% | +21.80% | 47.6% | ↑ ↑ ↑ (3/3 su) | 37.0 (15° pctl 1a) |
| **ETH** | $2,526.45 | +0.39% | +1.83% | +34.02% | 70.8% | ↑ ↑ ↑ (3/3 su) | 52.6 (26° pctl 1a) |
## Libro
Equity **$4,468.59** · nozionale lordo $1,134 · leva lorda 0.25x · barra dati 2026-09-12 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.411 | flat | $+688 | $+688 | HOLD (a target) |
| **ETH** | +0.267 | flat | $+447 | $+446 | HOLD (a target) |
## P&L
- giorno: **$+2.43** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+3,870.53** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+31.56** · TWR **+7.09%**
## 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 $+688 (TP01 +0.411, SKH01 flat) e ETH $+446 (TP01 +0.267, SKH01 flat).
- `[leva]` Leva lorda **0.25x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.75x.
- `[pnl]` Equity $+2.43 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.78%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.0 contro realizzata 30g 47.6 (sotto); DVOL al 15° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.6 contro realizzata 30g 70.8 (sotto); DVOL al 26° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **81 giorni, 55 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-13T00:38:21+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto nuovo è l'assenza di attività: zero fill e zero round-trip, dopo gli 8 fill e i 13 round-trip dell'11/09 — 13 dei 55 round-trip dell'intero campione di 81 giorni stanno in quella sola giornata. Oggi il libro ha solo tenuto: BTC $+688 ed ETH $+446, entrambi a target, e i $+2.43 sono marcatura su posizioni già aperte.
L'esposizione è tutta TP01: SKH01 è flat su entrambe le gambe, quindi la leva 0.25x contro un tetto di 1.00x descrive un solo sleeve dei due, non un libro deliberatamente piccolo.
Il −0.78% dal picco di $4,503.52 ammette due letture ugualmente compatibili: le quattro giornate negative dal 07 al 10/09, oppure la semplice marcatura di due long mentre BTC fa −3.19% su sette giorni contro +21.80% su trenta. Nulla di quel calo è stato realizzato — oggi zero fill, e il trading dall'arming resta $+31.56. Quale delle due pesi di più non è misurabile qui.
Salute piena: 24/24 giri, feed SKH a 0 minuti. Per l'occhio umano: il TWR +7.09% convive con $+3,838.97 di versamenti contro $+31.56 di trading, ed è il secondo numero, non il primo, a dire qualcosa del libro.
## Nota
*(vuota — campo dell'operatore)*
+60
View File
@@ -0,0 +1,60 @@
# Giornale di bordo — 2026-09-13
*Scritto 2026-09-14T00:37:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $76,808.50 | -0.62% | -4.44% | +21.93% | 47.6% | ↑ ↑ ↑ (3/3 su) | 38.9 (34° pctl 1a) |
| **ETH** | $2,476.90 | -1.96% | -1.52% | +31.73% | 71.4% | ↑ ↑ ↑ (3/3 su) | 54.3 (39° pctl 1a) |
## Libro
Equity **$4,455.99** · nozionale lordo $1,134 · leva lorda 0.25x · barra dati 2026-09-13 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.412 | flat | $+689 | $+691 | HOLD (a target) |
| **ETH** | +0.267 | flat | $+446 | $+443 | HOLD (a target) |
## P&L
- giorno: **$-12.60** di equity (24 letture)
- realizzato: -0.19 netto su 1 round-trip chiusi · 4 fill · fee 0.0101
- cumulato dall'arming: **$+3,857.93** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+18.96** · TWR **+6.79%**
## 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 $+691 (TP01 +0.412, SKH01 flat) e ETH $+443 (TP01 +0.267, SKH01 flat).
- `[leva]` Leva lorda **0.25x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.75x.
- `[pnl]` Equity $-12.60 con 4 fill e 1 round-trip chiusi (realizzato $-0.19 netto).
- `[drawdown]` 📌 Equity **-1.06%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 38.9 contro realizzata 30g 47.6 (sotto); DVOL al 34° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 54.3 contro realizzata 30g 71.4 (sotto); DVOL al 39° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **82 giorni, 56 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-14T00:38:00+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Quarta giornata di fila senza decisione nuova: TP01 resta pieno su entrambe le gambe (+0.412 BTC, +0.267 ETH), SKH01 flat, posizioni a target, leva 0.25x. Il $-12.60 è quasi tutto marcatura — il realizzato è $-0.19 su 1 round-trip, 4 fill, fee 0.0101 — e ha la stessa forma dei cali del 07-10/09 (-18.75, -6.72, -5.36, -17.24). A $4,455.99 il libro sta appena sopra il minimo del 10/09 ($4,454.86) e -1.06% sotto il picco del 06/09.
L'insolito sta nell'attività: 13 round-trip l'11/09, zero il 12/09, 1 oggi, a posizione sostanzialmente ferma. Sono ri-taglie attorno al target, non cambi di vista.
Il contrasto che vale la pena registrare: BTC -4.44% su 7 giorni ed ETH -1.96% nel giorno, ma TSMOM 3/3 su per entrambi e +21.93%/+31.73% a 30 giorni. Il segnale lungo non vede la settimana — per costruzione. Lo stesso dato regge due letture ugualmente compatibili: ritirata dentro un trend intatto, oppure inizio di un giro che il filtro a 30/90/180 giorni vedrà con ritardo. Oggi non si separano.
Sulla vol, implicita sotto realizzata su entrambi (38.9 vs 47.6; 54.3 vs 71.4) con DVOL al 34°/39° percentile: VRP01 resta chiuso dal gate (0.11 e 0.16 contro 0.30).
Salute piena, 24/24 giri, feed a 0 min. Nulla da controllare a mano. Con 82 giorni e 56 round-trip, questi cali restano rumore, non evidenza.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-14
*Scritto 2026-09-15T00:37:38+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $78,192.50 | +1.80% | -1.15% | +24.05% | 47.7% | ↑ ↑ ↑ (3/3 su) | 38.6 (30° pctl 1a) |
| **ETH** | $2,516.15 | +1.58% | +1.04% | +33.78% | 71.4% | ↑ ↑ ↑ (3/3 su) | 54.9 (42° pctl 1a) |
## Libro
Equity **$4,477.74** · nozionale lordo $1,130 · leva lorda 0.25x · barra dati 2026-09-14 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.410 | flat | $+689 | $+689 | HOLD (a target) |
| **ETH** | +0.265 | flat | $+444 | $+441 | HOLD (a target) |
## P&L
- giorno: **$+21.75** di equity (24 letture)
- realizzato: -0.69 netto su 5 round-trip chiusi · 7 fill · fee 0.0189
- cumulato dall'arming: **$+3,879.68** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+40.71** · TWR **+7.31%**
## 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 $+689 (TP01 +0.410, SKH01 flat) e ETH $+441 (TP01 +0.265, SKH01 flat).
- `[leva]` Leva lorda **0.25x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.75x.
- `[pnl]` Equity $+21.75 con 7 fill e 5 round-trip chiusi (realizzato $-0.69 netto).
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 38.6 contro realizzata 30g 47.7 (sotto); DVOL al 30° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 54.9 contro realizzata 30g 71.4 (sotto); DVOL al 42° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **83 giorni, 61 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-15T00:38:16+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il +$21.75 è il delta giornaliero più ampio fra i dieci giorni elencati — il migliore precedente era +$12.27 del 06/09 — e arriva interamente dalla marcatura: il realizzato è **negativo**, $-0.69 su 5 round-trip chiusi con 7 fill e fee 0.0189. Lo stesso dato regge due letture ugualmente compatibili: il libro ha catturato il +1.80% di BTC e il +1.58% di ETH stando long su entrambi, oppure due posizioni ferme sono state semplicemente rivalutate da un mercato che saliva. Con 83 giorni e 61 round-trip non si distingue.
Il fatto insolito è la direzione del nozionale: dal 04/09 BTC scende da $+717 a $+689 e ETH da $+456 a $+441 mentre i prezzi salgono e TSMOM resta 3/3 su entrambi. Le cause — segnale che si assottiglia, equity che si è ritirata dal picco di $4,503.52 — non sono separabili qui: i segnali dei giorni precedenti non compaiono nella pagina.
Resta l'incoerenza di fondo con cui convivere: l'equity è a $4,477.74 e il cumulato $+3,879.68, ma il trading da arming vale $+40.71 con TWR +7.31%.
Meriterebbe uno sguardo perché cinque round-trip chiusi lascino un realizzato negativo in una giornata in salita: se sono ritagli di taglia va bene, se sono uscite no. Salute piena, 24/24 giri, feed a 0 minuti.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-15
*Scritto 2026-09-16T00:36:40+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $75,607.00 | -3.31% | -3.65% | +20.31% | 49.7% | ↑ ↑ ↑ (3/3 su) | 39.0 (35° pctl 1a) |
| **ETH** | $2,397.35 | -4.72% | -3.54% | +27.89% | 74.1% | ↑ ↑ ↑ (3/3 su) | 55.2 (43° pctl 1a) |
## Libro
Equity **$4,433.75** · nozionale lordo $568 · leva lorda 0.13x · barra dati 2026-09-15 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.410 | SHORT @ 75,707.5 | $+128 | $+129 | HOLD (a target) |
| **ETH** | +0.265 | flat | $+440 | $+439 | HOLD (a target) |
## P&L
- giorno: **$-43.99** di equity (24 letture)
- realizzato: -22.53 netto su 11 round-trip chiusi · 8 fill · fee 0.2114
- cumulato dall'arming: **$+3,835.69** di equity — di cui $+3,838.97 versati/prelevati -> trading **$-3.28** · TWR **+6.25%**
## 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 $+129 (TP01 +0.410, SKH01 short) e ETH $+439 (TP01 +0.265, SKH01 flat).
- `[leva]` Leva lorda **0.13x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.87x.
- `[pnl]` Equity $-43.99 con 8 fill e 11 round-trip chiusi (realizzato $-22.53 netto).
- `[drawdown]` 📌 Equity **-1.55%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 39.0 contro realizzata 30g 49.7 (sotto); DVOL al 35° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 55.2 contro realizzata 30g 74.1 (sotto); DVOL al 43° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **84 giorni, 72 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-16T00:37:25+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto nuovo non è la perdita, è la forma della posizione: il netto su BTC scende da $+689 a $+129 perché SKH01 è passato SHORT a 75,707.5 contro il long TP01 (+0.410), e la leva lorda cade a 0.13x. Il libro è a target su entrambe le gambe, non tagliato da un cap; ETH resta $+439 con SKH01 flat.
I $-43.99 di equity sono il giorno peggiore della finestra mostrata — il precedente era $-18.75 il 07/09 — e solo metà è realizzata: $-22.53 netti su 11 round-trip e 8 fill, fee 0.2114. Il resto è marcatura, in una giornata da BTC -3.31% e ETH -4.72% con i TSMOM ancora 3/3 su e il 30g a +20.31% e +27.89%: il libro perde dentro un trend che per le sue regole non è girato. L'implicita resta sotto la realizzata su entrambi (39.0 contro 49.7, 55.2 contro 74.1) e il gate di VRP01 resta chiuso.
Lettura alternativa altrettanto compatibile: lo short su BTC ha ridotto l'esposizione proprio mentre il prezzo scendeva. Se abbia attenuato o aggravato la giornata non è misurabile qui — la pagina non scompone il P&L per sleeve.
Merita un occhio umano: il trading dall'arming è ora $-3.28, negativo, mentre il TWR resta +6.25%. A 84 giorni e 72 round-trip è esattamente quanto il campione permette di dire.
## Nota
*(vuota — campo dell'operatore)*
+56
View File
@@ -0,0 +1,56 @@
# Giornale di bordo — 2026-09-16
*Scritto 2026-09-17T00:38:45+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $76,167.50 | +0.74% | -2.70% | +18.11% | 49.1% | ↑ ↑ ↑ (3/3 su) | 36.2 (11° pctl 1a) |
| **ETH** | $2,416.45 | +0.80% | -2.09% | +26.36% | 74.0% | ↑ ↑ ↑ (3/3 su) | 52.8 (27° pctl 1a) |
## Libro
Equity **$4,438.59** · nozionale lordo $537 · leva lorda 0.12x · barra dati 2026-09-16 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.399 | SHORT @ 75,707.5 | $+109 | $+107 | HOLD (a target) |
| **ETH** | +0.257 | flat | $+427 | $+430 | HOLD (a target) |
## P&L
- giorno: **$+4.84** di equity (24 letture)
- realizzato: -2.08 netto su 2 round-trip chiusi · 2 fill · fee 0.0123
- cumulato dall'arming: **$+3,840.53** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+1.56** · TWR **+6.37%**
## 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 $+107 (TP01 +0.399, SKH01 short) e ETH $+430 (TP01 +0.257, SKH01 flat).
- `[leva]` Leva lorda **0.12x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.88x.
- `[pnl]` Equity $+4.84 con 2 fill e 2 round-trip chiusi (realizzato $-2.08 netto).
- `[drawdown]` 📌 Equity **-1.44%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.2 contro realizzata 30g 49.1 (sotto); DVOL al 11° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.8 contro realizzata 30g 74.0 (sotto); DVOL al 27° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **85 giorni, 74 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-17T00:39:34+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il cambiamento non è di oggi ma del 2026-09-15. Quel giorno la posizione BTC scende da $+689 a $+129, con 8 fill e la variazione peggiore della serie, -43.99. Oggi la posizione vale $+107, con SKH01 SHORT @ 75,707.5 contro un TP01 ancora a +0.399. La lettura più semplice è che lo short del breakout netti quasi tutto il long di trend. Però la pagina mostra i segnali solo per oggi: quando sia entrato lo short, e quanto dei -43.99 venga dal mercato e quanto dall'esecuzione, non è misurabile qui. L'esposizione è ora quasi tutta su ETH ($+430), e la chiusura BTC a $76,167.50 sta sopra l'ingresso dello short.
Dal 2026-09-06 l'equity è passata da 4,502.93 a 4,438.59. Il libro ha tenuto i long mentre BTC ed ETH segnavano -2.70% e -2.09% a 7 giorni. È compatibile con una pausa dentro un trend che a 30 giorni resta +18.11% e +26.36%, ma anche con il suo esaurimento: i dati non distinguono le due letture. Il trading a $+1.56 accanto al TWR +6.37% non è una contraddizione: il TWR pesa i periodi, i dollari pesano il capitale di ciascun periodo.
Merita un controllo umano il conteggio dei round-trip. Il 2026-09-11 e il 2026-09-15 supera quello dei fill (13 su 8, 11 su 8). Inoltre la taglia di capitale scritta nelle istruzioni dell'analista non coincide con l'equity della pagina.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-17
*Scritto 2026-09-18T00:39:56+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $76,380.00 | +0.28% | -0.22% | +18.06% | 49.1% | ↑ ↑ ↑ (3/3 su) | 33.6 (0° pctl 1a) |
| **ETH** | $2,446.05 | +1.22% | +0.35% | +27.59% | 74.0% | ↑ ↑ ↑ (3/3 su) | 50.1 (13° pctl 1a) |
## Libro
Equity **$4,444.68** · nozionale lordo $537 · leva lorda 0.12x · barra dati 2026-09-17 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.399 | SHORT @ 75,707.5 | $+109 | $+107 | HOLD (a target) |
| **ETH** | +0.257 | flat | $+428 | $+430 | HOLD (a target) |
## P&L
- giorno: **$+6.09** di equity (24 letture)
- realizzato: -0.37 netto su 1 round-trip chiusi · 1 fill · fee 0.0018
- cumulato dall'arming: **$+3,846.62** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+7.65** · TWR **+6.52%**
## 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 $+107 (TP01 +0.399, SKH01 short) e ETH $+430 (TP01 +0.257, SKH01 flat).
- `[leva]` Leva lorda **0.12x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.88x.
- `[pnl]` Equity $+6.09 con 1 fill e 1 round-trip chiusi (realizzato $-0.37 netto).
- `[drawdown]` 📌 Equity **-1.31%** sotto il picco ($4,503.52). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 33.6 contro realizzata 30g 49.1 (sotto); DVOL al 0° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.1 contro realizzata 30g 74.0 (sotto); DVOL al 13° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **86 giorni, 75 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-18T00:40:39+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il cambiamento che conta non è di oggi ma del 15/09. Quel giorno la posizione BTC è passata da $+689 a $+129, e oggi è a $+107. ETH invece resta fra $+430 e $+453, come dal 07/09. TP01 BTC è ancora a +0.399 e SKH01 è SHORT @ 75,707.5. Il calo è quindi compatibile con la gamba breakout che compensa quella di trend. Se sia sceso anche TP01 non è misurabile qui, perché i giorni precedenti non riportano i segnali. Con leva lorda a 0.12x, oggi il libro è quasi solo ETH. BTC chiude a $76,380.00, sopra il prezzo d'ingresso dello short, con TSMOM 3/3 su: la gamba SKH01 va contro il trend che TP01 segue.
Il $+6.09 del giorno non si può dividere qui fra le due gambe e il collaterale. Dal 07/09 l'equity scende da $4,484.18 a $4,444.68. Il solo 15/09 (-43.99) pesa più di tutto il trading dall'arming, che vale $+7.65 contro $+3,838.97 versati: basta un giorno a cambiare il segno del risultato cumulato.
È insolito il DVOL BTC al 0° pctl di un anno con una RV30 al 49.1%. Due letture sono ugualmente compatibili: o l'implicita è compressa, o la RV30 contiene ancora il +18.06% del mese mentre la settimana è piatta (-0.22%).
Da controllare: l'11/09 e il 15/09 i round-trip (13 e 11) sono più dei fill (8 e 8). Va chiarito se dipende da come vengono abbinati i fill o da un errore di conteggio.
## Nota
*(vuota — campo dell'operatore)*
+60
View File
@@ -0,0 +1,60 @@
# Giornale di bordo — 2026-09-18
*Scritto 2026-09-19T00:39:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $80,907.00 | +5.93% | +4.77% | +16.70% | 47.4% | ↑ ↑ ↑ (3/3 su) | 35.5 (7° pctl 1a) |
| **ETH** | $2,612.25 | +6.79% | +3.80% | +15.98% | 53.9% | ↑ ↑ ↑ (3/3 su) | 49.9 (13° pctl 1a) |
## Libro
Equity **$4,526.26** · nozionale lordo $1,444 · leva lorda 0.32x · barra dati 2026-09-18 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.452 | flat | $+768 | $+769 | HOLD (a target) |
| **ETH** | +0.401 | flat | $+681 | $+675 | BUY $+6 |
## P&L
- giorno: **$+81.58** di equity (24 letture)
- realizzato: -0.70 netto su 8 round-trip chiusi · 12 fill · fee 0.3354
- cumulato dall'arming: **$+3,928.20** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+89.23** · TWR **+8.47%**
## 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 $+769 (TP01 +0.452, SKH01 flat) e ETH $+675 (TP01 +0.401, SKH01 flat).
- `[leva]` Leva lorda **0.32x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.68x.
- `[pnl]` Equity $+81.58 con 12 fill e 8 round-trip chiusi (realizzato $-0.70 netto).
- `[concentrazione]` 📌 **Il 78% di tutto il P&L cumulato ($+89.23) viene dagli ultimi 7 giorni.** Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[vol]` BTC: implicita 35.5 contro realizzata 30g 47.4 (sotto); DVOL al 7° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.9 contro realizzata 30g 53.9 (sotto); DVOL al 13° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +5.93%, **2.4 deviazioni** giornaliere (sd implicita dalla RV30 = 2.48%).
- `[evento]` 📌 ETH: giornata a +6.79%, **2.4 deviazioni** giornaliere (sd implicita dalla RV30 = 2.82%).
- `[evidenza]` 📌 Campione a oggi: **87 giorni, 83 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-19T00:40:10+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il cambiamento della giornata sta nelle posizioni. Dal 15 al 17 settembre BTC era sceso a $+129 e poi a $+107, mentre ETH restava intorno a $+430. Oggi entrambe le gambe sono tornate a taglia piena, BTC $+769 ed ETH $+675, con SKH01 flat su tutti e due. La pagina non dice perché BTC fosse stato ridotto, né se il ritorno sia avvenuto prima o dopo il rialzo. Se la riduzione del 15 sia costata o abbia protetto non è misurabile qui.
I $+81.58 del giorno vengono dalla marcatura delle posizioni aperte, non dalle chiusure: gli 8 round-trip hanno realizzato $-0.70 netti. Da soli pesano quasi quanto l'intero trading dall'arming, $+89.23. La concentrazione segnalata è quindi, in sostanza, quella di un solo giorno. Esiste una lettura altrettanto compatibile: un libro lungo su entrambi gli asset ha semplicemente incassato una giornata da 2.4 deviazioni, e questo non dice nulla sul segnale.
È insolito anche il contesto di volatilità. Il DVOL sta al 7° e al 13° percentile di un anno, sotto la realizzata, proprio nel giorno di un movimento ampio. Se l'implicita abbia reagito non si vede, perché la pagina non riporta i DVOL dei giorni precedenti.
Da controllare: il capitale indicato nelle istruzioni dell'analista non corrisponde all'equity di $4,526.26 di questa pagina.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-19
*Scritto 2026-09-20T00:37:23+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $81,279.50 | +0.46% | +5.16% | +11.33% | 44.3% | ↑ ↑ ↑ (3/3 su) | 36.3 (13° pctl 1a) |
| **ETH** | $2,632.95 | +0.79% | +4.22% | +13.16% | 53.0% | ↑ ↑ ↑ (3/3 su) | 50.8 (17° pctl 1a) |
## Libro
Equity **$4,536.47** · nozionale lordo $1,385 · leva lorda 0.31x · barra dati 2026-09-19 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.443 | flat | $+754 | $+756 | HOLD (a target) |
| **ETH** | +0.372 | flat | $+632 | $+629 | HOLD (a target) |
## P&L
- giorno: **$+10.21** di equity (24 letture)
- realizzato: +1.00 netto su 5 round-trip chiusi · 5 fill · fee 0.0316
- cumulato dall'arming: **$+3,938.41** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+99.44** · TWR **+8.72%**
## 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 $+756 (TP01 +0.443, SKH01 flat) e ETH $+629 (TP01 +0.372, SKH01 flat).
- `[leva]` Leva lorda **0.31x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.69x.
- `[pnl]` Equity $+10.21 con 5 fill e 5 round-trip chiusi (realizzato $+1.00 netto).
- `[concentrazione]` 📌 **Il 70% di tutto il P&L cumulato ($+99.44) viene dagli ultimi 7 giorni.** Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[vol]` BTC: implicita 36.3 contro realizzata 30g 44.3 (sotto); DVOL al 13° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.8 contro realizzata 30g 53.0 (sotto); DVOL al 17° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **88 giorni, 88 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-20T00:37:51+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Giornata di consolidamento dopo lo strappo del 18/09: il libro resta a target su entrambe le gambe (BTC $+756 contro $+754, ETH $+629 contro $+632) e i 5 fill valgono $+1.00 realizzato netto con fee 0.0316 — ritagli, non decisioni. Il $+10.21 di equity è quasi tutto marcatura delle due long.
Quel che le regole non vedono è la forma della settimana. Dal 15 al 17 settembre la gamba BTC era ridotta a $+129 e poi $+107: il libro era sostanzialmente fuori dal rischio direzionale sull'asset principale. Il rientro pieno è del 18, lo stesso giorno del $+81.58. Da qui la concentrazione segnalata dalla regola `[concentrazione]` ha due letture ugualmente compatibili: TP01 ha ripreso un trend che ora conferma 3/3 su entrambi gli asset, oppure il rientro ha semplicemente coinciso con un rimbalzo e il merito è del calendario. Con 88 giorni e 88 round-trip non è distinguibile qui.
L'elemento insolito è la volatilità: implicita sotto la realizzata su entrambi (36.3 contro 44.3, 50.8 contro 53.0) con DVOL al 13° e 17° percentile dell'anno. Il gate di VRP01 resta chiuso e nessuna azione ne discende, ma la combinazione merita un'occhiata umana se persiste.
Salute piena: 24/24 giri, feed a 0 min.
## Nota
*(vuota — campo dell'operatore)*
+58
View File
@@ -0,0 +1,58 @@
# Giornale di bordo — 2026-09-20
*Scritto 2026-09-21T00:39:45+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $81,197.50 | -0.10% | +5.71% | +3.63% | 37.1% | ↑ ↑ ↑ (3/3 su) | 35.6 (8° pctl 1a) |
| **ETH** | $2,645.45 | +0.47% | +6.80% | +5.10% | 45.7% | ↑ ↑ ↑ (3/3 su) | 51.5 (21° pctl 1a) |
## Libro
Equity **$4,536.18** · nozionale lordo $1,648 · leva lorda 0.36x · barra dati 2026-09-20 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.533 | flat | $+906 | $+909 | HOLD (a target) |
| **ETH** | +0.435 | flat | $+739 | $+739 | HOLD (a target) |
## P&L
- giorno: **$-0.29** di equity (24 letture)
- realizzato: +0.10 netto su 1 round-trip chiusi · 5 fill · fee 0.0985
- cumulato dall'arming: **$+3,938.12** di equity — di cui $+3,838.97 versati/prelevati -> trading **$+99.15** · TWR **+8.71%**
## 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 $+909 (TP01 +0.533, SKH01 flat) e ETH $+739 (TP01 +0.435, SKH01 flat).
- `[leva]` Leva lorda **0.36x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.64x.
- `[pnl]` Equity $-0.29 con 5 fill e 1 round-trip chiusi (realizzato $+0.10 netto).
- `[concentrazione]` 📌 **Il 69% di tutto il P&L cumulato ($+99.15) viene dagli ultimi 7 giorni.** Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[vol]` BTC: implicita 35.6 contro realizzata 30g 37.1 (sotto); DVOL al 8° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.5 contro realizzata 30g 45.7 (sopra); DVOL al 21° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **89 giorni, 89 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-21T00:40:36+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
Il fatto nuovo non e' il P&L — $-0.29 su 24 letture — ma l'esposizione: BTC $+909 ed ETH $+739 sono i valori piu' alti dei dieci giorni elencati, e arrivano con l'equity ferma (ieri $4,536.47). Non e' capitale nuovo: e' TP01 che spinge (+0.533 e +0.435) dopo aver quasi lasciato BTC fra il 15 e il 17/09 ($+129, poi $+107). Il libro ha ricomprato quel rischio tre giorni dopo, e resta a 0.36x contro un tetto di 1.00x: il vincolo qui e' il segnale, non il cap.
Sulla concentrazione la regola dice il fatto, non la causa. Il 69% del cumulato $+99.15 cade nei sette giorni in cui BTC ha fatto +5.71% ed ETH +6.80% con il libro long: «un direzionale pagato da un rally» e' compatibile con questi numeri quanto «l'edge che si vede». A 89 giorni e 89 round-trip le due letture non si separano, e non sono separabili qui.
I due asset non concordano di segno sulla volatilita': implicita sotto la realizzata su BTC (35.6 contro 37.1), sopra su ETH (51.5 contro 45.7). VRP01 resta comunque fermo per l'IV-rank (0.03 e 0.13 contro 0.30), cioe' per l'espansione, non per il livello.
Merita un occhio umano il rapporto fra realizzato $+0.10 e fee 0.0985 su 5 fill. Salute pulita: 24/24 giri, feed a 0 min.
## Nota
*(vuota — campo dell'operatore)*
+34 -2
View File
@@ -69,7 +69,11 @@ chi lo riapre deve battere il motivo, non ripetere l'esperimento.
solo LOWVOL 19-major regge standalone (FULL/HOLD 1.07) ma deflated-Sharpe 0.13 + storia 2.5a → **DEBOLE/
forward STAT-MODE** (`2026-06-29-xsec-v2-nonmom.md`). (D) **MACRO regime-gate** (equity/credito/oro/tassi →
de-risk crypto): **RIDONDANTE col trend** (corr→TP01 0.989; il gate lavora solo nel 2-3% dei giorni, TP01
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`).
già flat nei crash) → SCARTATO (`2026-06-29-macro-regime-gate.md`). (D-bis, 10/09) **MACRO RISK-OFF ±2h intorno a
FOMC/CPI su TP01** (`r0910_macro_riskoff.py`, §78): **REFUTED 0/3** — ΔSh −0,097, drift −1,24%/anno, Δ vero al
4,2° pctl del null location-matched: nell'1% delle ore in finestra TP01 fa il 7% del guadagno (+1,05 bp/h
contro +0,17). Come il weekend: la coda del trend vive nelle ore «pericolose». Un gate «fondamentale»
che rifiuta i segnali intorno ai dati costerebbe quanto il funding, con segno scelto da chi lo propone.
- **Sweep strategie a 5 thread (2026-06-29) — 0 nuovi sleeve, 1 LEAD che rompe 2 muri su 3.** Ricerca
parallela onesta su aree inesplorate (harness `altlib`+`xsec_v2_nonmom`, tutti i gate incl. il nuovo
@@ -701,7 +705,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%**
@@ -1521,3 +1525,31 @@ una maschera a caso della stessa frequenza**.
**Gated sull'IV sottile** (la copertura dinamica che §46 non aveva provato): migliora, perde.
**Cosa lo riapre:** scadenza lunga che copra il grind (fuori dal vincolo ≤15g dell'operatore),
o un premio di varianza negativo su BTC. Non delta, tenor ≤15, gate IV: misurati.
- **CROLLI / CROLLO CATTURATO / XRP — 0 candidati, 1 debito, 2 conclusioni smontate in revisione (2026-09-09)** —
`r0909_{libro_nei_crolli,crash_catturato,xrp_terza_gamba,deribit_universo}.py`, diario
`2026-09-09-crolli-opzioni-monete.md`. Domanda dell'operatore: *"guadagnare anche nei crolli (opzioni se
serve), piu' monete ma sempre in Deribit"*. **(1) Il libro nei crolli**: perde nel GIORNO (−0,48%/g su 160
giorni ≤ −5%, positivo nel 18%), immune-o-positivo per finestra (4 POSITIVO / 6 immune / 2 PERDE su 12), **> 0
in 8/8 peggiori finestre dal 2022** per la gamba short di SKH01 (108% dei guadagni, scomposizione esatta,
marcata all'uscita). Beta 0,0769 sulla finestra di §46 (riprodotto). Peggior giorno canonico 2025-10-10
−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,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);
ingressi durante il crollo bimodali (perde il 46%). **Crollo a vol BASSA** (picco DVOL 29°/46° pctl), gate
IV-rank di VRP01 chiuso: **la condizione di §3 e' soddisfatta nella forma, non nella sostanza → non si
riapre**. BTC pre-crollo non misurabile (griglia strike rada). **(3) XRP terza gamba** (harness di
`r0822_sol_leg`, congelato): **diluisce come SOL** — hold-out −0,169 in 0/24, FULL −0,206 (L-FULL) /
−0,066 (2024+), un anno buono (2024), de-levering; L-PULITA dal 2024 (2023 0,57% >1% dal riferimento;
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 §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.
+96 -3
View File
@@ -703,9 +703,16 @@ e i vincoli di deploy (PRIIPs/UCITS/broker).
movimento certo e moltiplica i segmenti (gli ambigui non spezzano: restano nel rendimento,
dichiarati accanto — P12). Il report stampa TWR, segmenti con le date, movimenti elencati,
trading al netto, e il delta $ grezzo solo etichettato «movimenti INCLUSI». Verifica M23: la
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Limite ereditato:** un
+10% di trading fra due letture orarie consecutive, a mercato fermo, sarebbe classificato
movimento — e' il limite dichiarato del rilevatore live (D5), non uno nuovo.
macchina riproduce il +10,80% del diario 01/09 sui suoi quattro punti. **Due limiti trovati in revisione (02/09):** l'intervallo che contiene un movimento certo esce INTERO dal rendimento (il suo P&L di mercato va in `certi`; errore massimo META' del movimento, per costruzione; il 25/08 ~$0,5) e a base di equity zero `twr` e `trading` sono entrambi None, perche' il salto 0→X del primo versamento e' invisibile al classificatore. I segmenti a lunghezza zero (movimento nel primo intervallo, o due consecutivi) non si stampano. **Seconda tornata di revisione, stesso giorno:** il classificatore era **cieco ~23 ore al giorno** — il
feed 1h si ferma alle 00:00, `asof` dava la stessa barra alle due letture, mercato «fermo» = 0, e
qualunque calo ≥10% del giorno sarebbe stato un movimento (un crash stampato come prelievo, dal
25/08); con feed assente tutto era «ambiguo» e il report stampava `e1/e0-1` sotto l'etichetta TWR.
Ora «mercato non misurabile» è uno stato: `ambiguo` con la ragione, `twr`/`trading` None con motivo.
Il «limite +10% a mercato fermo» scritto la mattina era un artefatto della fixture piatta, cancellato.
`pnl_giorno` chiama `rendimento_twr` (la frase «entrambi la chiamano» è vera solo da questa tornata),
la pagina stampa il TWR, la testata Telegram qualifica il cumulato «di cui versati», e il bound di
leva include la scala e il tetto di codice. Resta aperto (§5.15): il bound guarda solo BTC/ETH, e il
31% dell'equity è USDE — un depeg pieno sarebbe scorporato come prelievo.
**Le tre fonti si INCROCIANO e non si sovrascrivono:** log (ora vera) x jsonl (i fill) x venue
(autorevole ma **TRONCA** — 1 trade su BTC, 0 su ETH). `reconcile()` riporta le divergenze e
**non ripara niente da solo**: fra due fonti che non concordano, una riparazione silenziosa e'
@@ -915,6 +922,56 @@ Sulle 4 finestre osservate 3 sono rientrate entro l'ora → il `:47` ne avrebbe
**Se al prossimo martedì il `:47` becca comunque la manutenzione, la previsione è sbagliata** e lo
slot non è quello descritto in `venue_probe.RELEASE_*`: rileggerlo prima di spostare ancora.
### La cadenza è ORARIA, e ora lo dicono tutte e tre le fonti (debito §5.7, chiuso il 2026-09-02)
Il docstring di `book_execute.py` prescriveva «ogni ~230 minuti» (la griglia di SKH01) mentre il cron
gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che l'accumulo di scatti del disaster-SL
rotolante è funzione della **cadenza del cron** (1h: BTC 0 / ETH 1 in 7-8 anni · 4h: BTC 2 · 24h: ETH 5
con 2 in 30 giorni), quindi chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro
dalla riga 1h (rotolante: BTC 0 / ETH 1) alla riga 4h (BTC 2 contro 1 del pavimento; la peggiore misurata è 24h, ETH 5). La riparazione è il docstring stesso — cadenza ORARIA, la ragione (giro
idempotente: girare più fitto della griglia non costa ordini, e compra latenza ≤1h per gli
ingressi/uscite software di SKH01 e un **controllo** orario del rotolante — che si ri-ancora solo oltre la tolleranza di `ensure_disaster_sl`, mark +5,263%/−4,762% o taglia >10%, quindi lo stop siede fra −33,5% e −26,5% dal mark corrente: la prima stesura diceva «si ri-ancora ogni ora» ed era sbagliata, corretta in revisione lo stesso giorno) e il divieto scritto —
più `tests/test_book_cadenza.py`, che **deriva** (P1) le tre dichiarazioni e le confronta: la riga
citata nel docstring, quella dichiarata nell'intestazione di `cron_book.sh` (`47 * * * *`) e la
crontab **installata** (`crontab -l`; se non è leggibile il test è SALTATO, non verde — P5). Il
parser accetta solo espressioni regolari e rifiuta il resto. Vincolo del minuto tondo verificato
(≠ `:00`). La regola generale è la stessa del `:47`: **una cadenza si difende nella sua ragione, e
un commento che dichiara una cadenza diversa da quella che gira si paga quando qualcuno lo "corregge"
nel verso sbagliato.**
### Il README ha pubblicato per 75 giorni la libreria che il progetto aveva dichiarato falsa
Ultimo commit del README: **2026-06-04**. Reset v2.0.0: **2026-06-19**. In mezzo, quindici giorni; dopo,
75 in cui la prima pagina del repository descriveva FADE/HONEST/PAIRS/TSMOM/SHAPE, i portafogli PORT01-06,
il paper trader su `strategies.yml` e l'esecuzione shadow su testnet — con Sharpe 7,84/10,06 e CAGR ~79%
presentati come risultati correnti. Metà dei file citati non esisteva più (`strategies.yml`,
`portfolios.yml`, `scripts/waste/`, `scripts/portfolios/`, `src/live/multi_runner.py`); i numeri erano
esattamente quelli che `CLAUDE.md` §2 elenca sotto «non citare», e provenivano dalla libreria che il
reset aveva dichiarato artefatto di un feed contaminato.
Nessun danno sui soldi — il README non decide niente. Il danno è di **citazione**, ed è la forma peggiore:
un lettore esterno (o un agente nuovo) parte da lì. **La lezione non è "tenere aggiornato il README"**,
che nessuno fa più di quanto già non faccia: è che **un reset invalida anche i documenti che nessuno
rilegge**, e l'inventario di cosa cita numeri morti va fatto il giorno del reset, quando si sa che sono
morti — non 75 giorni dopo, quando qualcuno ci inciampa per caso. Riscritto il 2026-09-02 contro lo stato
vero, con ogni riferimento a file verificato e i numeri nella lente di §2.
### Un flag sconosciuto non è l'azione di default (2026-09-02)
Nessuno dei 18 script di `scripts/live/` usava argparse: leggevano `sys.argv` con `in` e `index`,
quindi un flag sbagliato non era un errore — era il ramo `else`. Il costo si è misurato lo stesso
giorno, dentro una revisione: `trades_db.py --help` è caduto in `sync()` e ha riscritto
`meta.ultimo_sync`. Sui due script che scrivono davvero sarebbe stato peggio: la pagina di giornale
e una riga di DB, oppure una chiamata al modello più un Telegram. **La riparazione non è argparse** —
cambierebbe messaggi d'errore, codici d'uscita e `--help` di script che il cron già chiama, cioè
romperebbe ciò che gira per riparare ciò che non gira mai. È una funzione di venti righe
(`src/live/cli.valida`) che rifiuta ciò che non è dichiarato ed esce **prima di qualunque effetto**:
`--help` → 0, flag ignoto → 2 con l'elenco dei previsti (P4). Il test deriva l'elenco degli script
dalla cartella (P1: un file nuovo senza guardia fallisce, non passa inosservato), controlla che
`valida` sia la prima istruzione di `__main__` (uscire dopo `connect()` sarebbe uscire dopo
l'effetto) e — l'altra metà del contratto, P15/P16 — che **i flag che il cron usa davvero restino
accettati**: una guardia che ferma il libro di bordo la notte stessa sarebbe peggio del difetto.
### Cosa resta scoperto
1. **La sonda dice di chi è il guasto, non lo aggira.** Col gateway giù il libro continua ad
@@ -954,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.
+22
View File
@@ -157,3 +157,25 @@
`fresh_5m`). **REGOLA: un guasto IN CORSO si misura al GIORNO PEGGIORE, non in media** — la prima
stesura confrontava "tutto" con "ultimi 7g" e diluiva un guasto di 2 giorni al 13.5%, commettendo
a 7 giorni lo stesso errore che dichiarava di evitare a 3 mesi. Diario `2026-07-30-vrp-quote-reali.md`.
- 🚨 **IL FEED 1h E' ETICHETTATO ALL'APERTURA, e `asof(ts)` guarda un'ora avanti (trovato in revisione
il 2026-09-09).** La barra 1h etichettata 20:00 chiude col 5m delle 20:55 (verificato: close 1h 20:00 =
close 5m 20:55 = 1603,5, mentre il 5m 19:55 fa 1572,95). Quindi `S.asof(ts)` su una serie indicizzata
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.
✅ **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`).
Certificazione per anno vs riferimento USD: **Coinbase non ha XRP fra 2021-01 e 2023-07** (delisting per
la causa SEC) → riferimento **Bitstamp** prima, Coinbase dopo; >1% 0,06% (2022) · 0,57% (2023) · ≤0,02%
(2024+); mediana 5-6 bps; **5m flat 27-61%** (BTC/ETH 0,1-2%, SOL 21%): il 5m e' illiquido. Cache del
riferimento in `data/_cache/` (gitignored, fuori backup: si riproduce con rete).
+100
View File
@@ -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"*.
@@ -3969,3 +3988,84 @@ quota > 0**: l'ultimo migliaio compra ~€100/anno e butta via l'assicurazione);
a banda larga, non un tasso (libro a mercato il 22% dei giorni, TWR +10,8% in 70g, hold-out TP01
~+0,05, fisco −30% a 10a); fondo d'emergenza da dichiarare. Operativamente nulla: cap dinamico,
rilevatore di salti collaudato il 25/08. **In attesa di importo e data.**
---
## 74 — LIBRO-NEI-CROLLI (cosa fanno TP01 e SKH01 nei crolli, misurato invece che ricordato)
`scripts/research/r0909_libro_nei_crolli.py`, diario `2026-09-09-crolli-opzioni-monete.md`. **Libro,
pesi, config, cron INVARIATI.** Lente: sleeve di ricerca, ancora canonica, pesi importati da
`src/live/book` (P1); ⚠️ SKH01 marcato all'USCITA del trade (zero nell'87,7% dei giorni). Indice 50/50;
giorno di crollo ≤ −5%; 12 peggiori finestre di 20 g non sovrapposte + 9 episodi nominati prima.
**Nel GIORNO del crollo il libro PERDE** (−0,48%/g su 160 giorni, positivo nel 18%; ≤ −10%: −0,51%/g con
SKH01 +0,42). **Per finestra**: 4 POSITIVO / 6 immune / 2 PERDE; libro > 0 in 8/12 e **in 8/8 dal 2022**
(mediana +3,3% contro indice −29%); le due perdite (2019, maggio 2021) sono TP01 long dentro un trend
non girato. **La gamba short di SKH01 fa il 108% dei guadagni** (scomposizione ESATTA long+short==intera,
assert). Beta **+0,0769** sulla finestra di §46 (riprodotto al millesimo). Peggior giorno canonico
2025-10-10 −3,38%: un crollo con TP01 a 0,505 (lo squeeze di §33 sta sulla lente hourly, non si riproduce
qui). 37,5/62,5 (§26) batte 12/12 episodi, Sharpe 1,81 vs 1,82, maxDD 12,1% vs 9,4%;
`r0726_reeval_live_weight` da' `gate_pass False` su `delta_insample −0,0026` (cache segnali ferma al
25/07: stesse cifre del 26/07, e la condizione che fallisce e' in-sample).
🚨 **Errore dell'autore smontato in revisione:** la prima stesura provava GUADAGNA prima di immune e
stampava **8/12 GUADAGNA**; nell'ordine dichiarato sono 4+6. *L'ordine delle regole era il verdetto.*
**VERDETTO: `PERDE NEL GIORNO, POSITIVO PER FINESTRA DAL 2022, TUTTO SKH01 SHORT`** — nessun meccanismo
nuovo: quello che guadagna nei crolli e' gia' in produzione, e la leva che lo aumenta (il peso) e' chiusa dal gate.
## 75 — CROLLO-CATTURATO (il f delle opzioni su un crollo a quote vere)
`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,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
non misurabile (griglia strike rada). **Crollo a vol BASSA**: picco DVOL al 29°/46° percentile, gate
IV-rank di VRP01 CHIUSO in tutte le ore. ⇒ la condizione di §3 e' soddisfatta nella forma, **non nella
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 §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)
`scripts/research/r0909_xrp_terza_gamba.py` (harness IMPORTATO da `r0822_sol_leg`). Dato in
`data/raw/alt_xrp_*` (namespace di ricerca, non rinfrescato dal cron). Certificazione: >1% dal
riferimento 0,06% (2022) · **0,57% (2023)** · 0,01-0,02% (2024+) — Coinbase dal rilisting 2023-07
(delisting SEC), Bitstamp prima; **L-PULITA dal 2024**, come SOL; 5m flat **27-61%**.
dSharpe hold-out **−0,169 in 0/24 ancore** (SOL −0,166); FULL −0,206 (L-FULL) / −0,066 (2024+); dCAGR
−2,27 / −1,32 pp; de-levering: L-FULL 1,23 vs 2g×0,974 **1,44**, 2024+ 1,54 vs 1,53 pari; per anno 2022
−0,55 · 2023 −0,66 · **2024 +0,54** · 2025 −0,44 · 2026 −0,25 (**1/5**). TP01 XRP Sharpe −0,01 /
hold-out −0,49; SKH01 XRP maxDD 40,1% (il criterio di selezione di SKH01-V2-DD fallisce, come SOL);
corr gamba-book +0,35/+0,37. Eseguibilita' non e' il vincolo (min 1 XRP = $1,42).
⚠️ Due difetti del verdetto trovati dai test (Opus) e corretti: campione vuoto stampava «DILUISCE»
(ora NON MISURABILE, P5); criterio per anno cablato al 2024 invece che derivato dalla lente (P1).
**VERDETTO: `XRP DILUISCE COME SOL — UN ANNO BUONO, HOLD-OUT 0/24`** — nessuna proposta.
## 77 — UNIVERSO-DERIBIT (lettura del venue, €0)
`scripts/research/r0909_deribit_universo.py` (API pubblica, JSON con ora in `data/_cache`, `--fresh`).
Lettura 2026-09-09 15:05Z: **32 perpetual USDC** aperti (+6 non aperti, fra cui **APT inactive**);
liquidi ≥ $10M/g **solo XRP ($38M, spread 4 bps), ETH, BTC, SOL ($13M)**; tutto il resto $0,01-7M
(TAO, HYPE, ADA, AVAX, NEAR…). XS01 **13/19** (mancano ARB, OP, APT, INJ, TIA, SEI → §50 resta).
Opzioni USDC-lineari su 7 sottostanti (SOL 676, HYPE 442, XRP 442, TRX, AVAX). Trovati in revisione:
**`BTCDVOL_USDC-30SEP26`** (unico long-vol diretto del venue: vol 0, OI 132, spread 12% → non
negoziabile) e **PAXG perp** (oro, dal 2024-12, $0,18M/g: non misurato, non morto, muri di SOL/XRP).
**VERDETTO: `QUATTRO STRUMENTI LIQUIDI, TRE GIA' NEL LIBRO O MISURATI`** — l'universo "piu' monete" di Deribit e' finito.
## 78 — MACRO-RISKOFF (TP01 flat ±2h intorno a FOMC e CPI: vale qualcosa?)
`scripts/research/r0910_macro_riskoff.py` (10/09, issue #9, ~3 min). Calendario letto dal web (Fed + BLS,
61 FOMC + 88 CPI, 2019-03 → 2026-09), 684 ore in finestra = 1,04% del tempo. Verdetto pre-registrato
(ΔSh>0 con fee · ≥95° pctl del null location-matched a 1.000 estrazioni · positivo in ≥70% degli anni).
**REFUTED 0/3**: ΔSharpe giornaliero **−0,097**, drift **−1,24%/anno** (≈ −0,14 €/g a $4.455), negativo in
6/8 anni, e il Δ vero sta al **4,2° percentile** del null — le ore intorno ai dati macro sono fra le MIGLIORI
in cui TP01 e' esposto (+1,05 bp/h contro +0,17, t = 1,57): l'1% del tempo porta il 7% del guadagno. Stessa
lezione del weekend (§3): la coda del trend vive nelle ore «pericolose». Il segno opposto NON si apre
(ipotesi nuova, sotto l'MDE, selezionata sull'esito). Diario `2026-09-10b-macro-riskoff.md`.
+3
View File
@@ -8,6 +8,9 @@
> `data/live/scale_history.jsonl`, `tests/test_book_scale.py` (T1-T11, **18 test verdi**).
> 🚨 **`config/live.json` NON e' stato toccato**, come prescrive il punto 2: la chiave e' assente,
> vale 1,00, e T7 dimostra bit-exact che il libro e' quello di sempre (`max|diff| = 0.0`).
> 📌 **(10/09, issue #5) A2 conta dal 2026-09-01**, primo giorno di `scale_watch`: 180 giorni cadono il
> **2027-02-28**, ed e' la data «non prima del» del gate in CLAUDE.md §4. Il «2026-10-01» scritto il 01/09 era
> il punto 5 (30 giorni di sorvegliante) scambiato per la data del gate: incompatibile con A2 per costruzione.
> **Restano i punti 5-7:** ≥30 giorni a 1,00 col sorvegliante attivo, poi `GATE SCALA-01`, poi
> `r0726_fee_sensitivity` rifatto. Il punto 5 non e' burocrazia — e' l'unico modo di scoprire che il
> sorvegliante e' rotto mentre la leva e' ancora 1,00.
+87
View File
@@ -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 | |
+40
View File
@@ -0,0 +1,40 @@
# Revisione settimanale — 2026-09-14
## Revisione NON eseguita
Il modello `claude-fable-5-1` non ha risposto: **uscita 1: You've reached your Fable limit. Switch to another model, or manage usage credits at claude.ai/settings/usage?from=cc_cli_limit_message, to continue.**. Non c'e' una revisione questa settimana, e questo file esiste perche' un lunedi' senza rapporto si leggerebbe come «tutto bene» (P5).
---
## 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) | 85,904 | |
| config/live.json (unica autorita' dei guardrail live) | 8,439 | |
| crontab installata (crontab -l) | 1,753 | |
| git log dal 2026-08-31 + stato del working tree | 3,693 | |
| monitor_health (i forward-monitor stanno registrando?) | 610 | |
| 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,415 | |
| stato usde_convert.jsonl (ultime 3) | 9,520 | |
| stato balance_watch.jsonl (ultime 2) | 579 | |
| stato venue_news.jsonl (ultime 5) | 1,564 | |
| stato movimenti_dichiarati.jsonl | 1,101 | |
| stato equity_seen.json (watermark) | 73 | |
| giornale 2026-09-07 | 3,846 | |
| giornale 2026-09-08 | 3,765 | |
| giornale 2026-09-09 | 3,705 | |
| giornale 2026-09-10 | 3,833 | |
| giornale 2026-09-11 | 2,348 | |
| giornale 2026-09-12 | 3,611 | |
| giornale 2026-09-13 | 3,823 | |
| 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 | 8,031 | |
| diario 2026-09-10-cinque-punti-revisione.md | 15,020 | |
| diario 2026-09-10b-macro-riskoff.md | 3,150 | |
+86
View File
@@ -0,0 +1,86 @@
# Revisione settimanale — 2026-09-16
*Scritta il 2026-09-16T19:39:30Z 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** — 7 numeri non presenti nel materiale: 1583.06, 1605.12, 1621.12, 5.11, 5.12, 5.16, 5.7.*
## Sintesi
- **URGENTE (conferma, non allarme sul saldo).** La crontab installata contiene dal 13/09 una riga nuova: `17 * * * * /opt/AI-OS/automations/TradingBook/scripts/cron_ombra.sh` («giro d'ombra del book TRASLOCATO … NON invia»). Non compare in CLAUDE.md (§1, §12), in nessun commit dal 02/09, ne' in `monitor_health`. Che non invii lo dice solo un commento. L'operatore confermi cosa legge (stesso gateway e stesso token del debito §5.11?), dove scrive, e che non tocchi `data/live` di questo progetto (P16).
- **Un sorvegliante morto 13 notti senza che nessuno lo vedesse**: commit `a6a49d9` (16/09) «cc01_regime_watch … moriva ogni notte dal 03/09 su from src.live.cli». Non e' nella tabella di `monitor_health`; e' un effetto collaterale della riparazione del debito §5.16 (02/09). P5: «non vedo» letto come «va bene».
- **Libro**: settimana di marcatura negativa; trading da arming **$-3.28** al 15/09 (giornale 2026-09-15), TWR +6.25%; SKH01 SHORT su BTC @ 75,707.5 dal 15/09, leva 0.13x. Esecuzione 24/24 ogni giorno, feed SKH 0 min. Nessun gate scade prima del **27/09 (STATARB)**.
- Il giornale **2026-09-11 non ha la sezione «Analisi (agente)»**: modello muto o errore, non determinabile dal materiale.
- Ripetute dal 09/09 e ancora aperte: contraddizione sull'haircut (M28); sorveglianti senza ora dell'ultimo giro.
## Cosa non torna
1. **Riga di cron non registrata** (crontab, commento datato 13/09/2026). Il progetto ha una regola scritta per questo caso: `tests/test_book_cadenza.py` legge la crontab «tutte le righe attive» ma cerca solo `cron_book.sh` (CLAUDE.md §5.7); un secondo processo orario sullo stesso conto e' fuori da ogni guardia. Il git log dal 02/09 non contiene un commit che lo descriva.
2. **`cc01_regime_watch`** (commit `a6a49d9`): 13 notti di traceback dal 03/09 al 16/09. `monitor_health` elenca 9 monitor e non questo. `tests/test_cli_flag.py` verifica che `valida` sia la prima istruzione (§5.16), non che l'import riesca lanciando lo script come fa il cron. Non determinabile se altri job di `cron_daily.sh` abbiano lo stesso difetto: lo `.sh` non e' nel materiale.
3. **Haircut, due testi che si contraddicono (M28).** CLAUDE.md §1: X:SM dal 31/08, «available_funds … $2.011,09, contro $2.010,90 attesi con haircut 5%» — cioe' l'haircut si applica. Piu' sotto, stesso §1: «haircut 5% … oggi comunque non si applica, vedi sopra». E `config/live.json` `_nota_usde` dice ancora «il modello di margine ATTIVO e' Segregated: Standard Margin (S:SM) … l'haircut oggi non si applica affatto», scritto prima del passaggio a cross. Segnalato il 09/09 (diario 2026-09-09c §5), non fra i cinque punti del 10/09.
4. **Giornale 2026-09-11: 13 round-trip chiusi con 8 fill, fee 0.4046**, posizioni a fine giornata quasi ferme (BTC $+688 → $+687, ETH $+449 → $+444). Il 15/09, con gli stessi 8 fill, la fee e' 0.2114 e il netto BTC e' passato da $+689 a $+129. **Dedotto**: l'11/09 c'e' stato un ciclo intraday che la pagina, fotografia di fine giornata, non mostra. L'analista del 14/09 ha posto la stessa domanda («se sono ritagli di taglia va bene, se sono uscite no») e resta senza risposta.
5. **Prompt dell'analista stantio**: l'Analisi del giornale 2026-09-10 scrive che la descrizione del libro nel prompt «indica un capitale molto inferiore all'equity di $4,454.86». Nessun commit successivo lo tocca; non determinabile se corretto.
6. **Sorveglianti senza prova di vita.** `scale_watch_state.json` non ha un timestamp e `scale_history.jsonl` ha la sola riga del 01/09: per A2 di SCALA-01 («criterio passato ogni giorno», §4) non c'e' nel materiale un'evidenza giornaliera. `venue_news.jsonl`: ultimo `visto_il` 2026-08-31, tutte righe `seed`: o nessuna notizia in 16 giorni o il lettore e' fermo, non distinguibile. `edge_watch`: ultimo stato citato 27/07 (§4), 51 giorni fa.
7. **Working tree**: sei pagine di giornale (10-15/09) e `docs/revisioni/2026-09-14.md` non committate, mentre `79afe41` mostra che i giornali si committano. La revisione del 14/09 non e' nel materiale: non determinabile se le sue segnalazioni siano state trattate.
8. **Cuscino USDC in erosione, dato mancante**: `usde_watch` eq_usdc 1621.12 (14/09) → 1605.12 (15/09) → 1583.06 (16/09), quota 0.6377 → 0.6431. `monitor_health` dice `cuscino_watch` OK, ma il suo record (slack, stato) non e' nel materiale. Non e' un allarme: e' un buco di lettura.
## Cosa e' maturo da decidere
| gate | data | criterio scritto | stato che il materiale mostra |
|---|---|---|---|
| **STATARB** | **2026-09-27** (fra 11 giorni) | soglie 24/07 + diagnostica statica sempre-short (§4) | Sharpe **−1,61** al 26/08; nessun valore piu' recente nel materiale; `paper_statarb` OK. Cade fra la revisione del 21/09 e quella del 28/09: **chi lo legge il 27/09 non e' stabilito** |
| `edge_watch` | continuo | (A) Sharpe 36m < −0,5 · (B) DD TP01 < 75% del buy&hold | ultimo stato 27/07 (+1,51, 8/8); nulla di piu' recente |
| USDE-01, PROP-01 | passati | — | chiusi (§4) |
Prossimi, fuori dai 30 giorni: XSR01 23/10, DVOLSPREAD kill 24/10. Nessuna condizione della colonna «cosa la riapre» di §3 risulta soddisfatta: `weights_tilt_null` fallisce ancora (26/07, cache al 25/07, diario 2026-09-09 §1); book a $4,433.75 contro la soglia $15k di GTAA01; stato di `fee_watch` assente dal materiale.
Decisioni dell'operatore: (a) registrare o togliere il giro d'ombra; (b) nominare chi legge STATARB il 27/09.
## Proposte (nessuna azione eseguita)
1. **Ogni job lanciato dal cron sotto `monitor_health`** o con un «ultimo esito + ora» leggibile, inclusi quelli interni a `cron_daily.sh` e il giro d'ombra. Verifica preliminare: confrontare l'elenco dei job in `cron_daily.sh` con le 9 righe di `monitor_health`.
2. **Un test che lanci ogni script di `scripts/live/` come lo lancia il cron** (processo separato, cwd diverso, `--help`): e' il caso #10 reso strutturale, nello spirito del debito §5.12 («non si chiede a ogni autore di ricordarsene»).
3. **Chiudere la contraddizione haircut** in §1 e in `_nota_usde`: un solo testo che dica quale modello e' attivo e se l'haircut si applica.
4. **Fill del giorno nella pagina di giornale** (ora, asset, verso, importo): chiesto il 09/09 come «registro fill della settimana»; avrebbe risposto da solo ai punti 4 e all'analista del 14/09.
5. **Timestamp dell'ultimo giro** in `scale_watch_state.json` e in `venue_news`: senza, A2 di SCALA-01 non e' dimostrabile alla sua data.
6. **Committare** giornali e revisione del 14/09.
Nessuna idea di strategia: nulla nel materiale batte i motivi di morte di `docs/memory/20-ondate-e-scartati.md`; `r0910_macro_riskoff` (REFUTED 0/3, diario 2026-09-10b) lo conferma per l'ennesimo risk-off.
## Cosa ho letto e cosa mi mancava
**Letto**: CLAUDE.md intero; `config/live.json`; crontab; git log dal 02/09 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 09-15/09; diari 2026-09-09, 09b, 09c, 10, 10b. Questo giro e' di mercoledi' 19:39Z, non del lunedi' 06:15: e' fuori cadenza.
**Mancava**: `docs/revisioni/2026-09-14.md` e l'esito delle sue segnalazioni · il contenuto di `cron_daily.sh` e di `cron_ombra.sh` · il record di `cuscino_watch` (slack e stato) · stati con ora di `edge_watch`, `fee_watch`, `venue_watch`, `scale_watch` · l'output di `usde_watch.rendimento()` col suo n · il valore a oggi delle serie paper (STATARB in particolare, a 11 giorni dal gate) · i fill della settimana · l'esito della suite dopo `a6a49d9` · il testo corrente del prompt dell'analista.
---
## 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) | 85,904 | |
| config/live.json (unica autorita' dei guardrail live) | 8,439 | |
| crontab installata (crontab -l) | 1,753 | |
| git log dal 2026-09-02 + stato del working tree | 1,680 | |
| monitor_health (i forward-monitor stanno registrando?) | 610 | |
| stato scale_watch_state.json | 149 | |
| stato scale_history.jsonl (ultime 3) | 259 | |
| stato vrp_f_watch/state.json | 454 | |
| stato usde_watch.jsonl (ultime 3) | 1,407 | |
| stato usde_convert.jsonl (ultime 3) | 9,520 | |
| stato balance_watch.jsonl (ultime 2) | 581 | |
| stato venue_news.jsonl (ultime 5) | 1,564 | |
| stato movimenti_dichiarati.jsonl | 1,101 | |
| stato equity_seen.json (watermark) | 73 | |
| giornale 2026-09-09 | 3,705 | |
| giornale 2026-09-10 | 3,833 | |
| giornale 2026-09-11 | 2,348 | |
| giornale 2026-09-12 | 3,611 | |
| giornale 2026-09-13 | 3,823 | |
| giornale 2026-09-14 | 3,736 | |
| giornale 2026-09-15 | 3,731 | |
| 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 | 8,031 | |
| diario 2026-09-10-cinque-punti-revisione.md | 15,020 | |
| diario 2026-09-10b-macro-riskoff.md | 3,150 | |
+94
View File
@@ -0,0 +1,94 @@
# Revisione settimanale — 2026-09-21
*Scritta il 2026-09-21T06:15:02Z 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** — 9 numeri non presenti nel materiale: 2253, 3.83897, 4.55027, 5.8, 6333, 75.7075, 80.907, 871, 9915.*
## Sintesi
- **URGENTE (credenziali, operatore).** L'ombra di TradingBook gira ogni ora con la stessa chiave che manda gli ordini del libro. La garanzia che non invii è solo il codice (CLAUDE.md §1 OMBRA: «la chiave può fare trading»). La chiave di sola lettura può crearla solo l'operatore. Nel materiale non c'è traccia che esista, e la finestra d'ombra dura fino al 30/09.
- Il libro è sano sui numeri: 24/24 giri ogni giorno dal 14/09 al 20/09. Equity, trading cumulato e conteggio dei round-trip si riconciliano giorno su giorno, per sottrazione mia, senza scarti. I versati ($3.838,97) coincidono con `movimenti_dichiarati.jsonl` più i $1.399,39 del 25/08.
- `cc01_regime_watch` è morto ogni notte dal 03/09 al 16/09 senza che nessuno lo vedesse (commit `a6a49d9`). `edge_watch`, `fee_watch`, `scale_watch`, `venue_watch`, `vrp_f_watch` e `venue_news` non compaiono nella tabella `monitor_health` del materiale. Due di loro reggono una decisione vincolante e il kill del libro.
- Il gate STATARB si legge fra 6 giorni, domenica 27/09. Le soglie non sono scritte in §4 e la prossima revisione cade il 28/09, dopo la lettura.
- Da due pagine di giornale l'analista segnala due anomalie senza risposta: le Note dell'operatore sono vuote 7/7 e non c'è nessun commit dopo il 17/09.
## Cosa non torna
| # | Cosa | Fonte | Tipo |
|---|---|---|---|
| 1 | **Sorveglianti fuori da `monitor_health`.** Il guasto di cc01 (`from src.live.cli`) parte il giorno dopo la riparazione del debito 16 del 02/09 (`cli.valida` in ogni script), e `test_cli_flag.py` non l'ha visto. La tabella `monitor_health` copre 10 voci, fra cui nessuno dei sorveglianti elencati nella Sintesi. Eppure §3 (fee) dice «`fee_watch` allerta da solo», e in §4 lo stato di `edge_watch` è del **27/07**. | commit `a6a49d9`, `31f5915`; CLAUDE.md §5.16, §3, §4; tabella monitor_health | Misurato: gli assenti dalla tabella. Dedotto: che possano essere morti allo stesso modo. |
| 2 | **Round-trip maggiori dei fill.** L'11/09 sono 13 su 8, il 15/09 sono 11 su 8. L'analista lo segnala due volte. Il 20/09 il rapporto si rovescia: 1 round-trip su 5 fill. Può essere l'abbinamento a lotti, ma nessuno l'ha verificato. | giornali 16/09, 17/09, 20/09 | Misurato il conteggio, non determinabile la causa. |
| 3 | **Capitale sbagliato nelle istruzioni dell'analista.** «La taglia di capitale scritta nelle istruzioni dell'analista non coincide con l'equity della pagina.» È un valore ridichiarato invece che derivato, cioè la forma di P1. | giornali 16/09 e 18/09; CLAUDE.md §6 P1 | Misurato dall'analista, non da me. |
| 4 | **Il P&L di SKH01 non è attribuibile.** Il 15/09 SKH01 va SHORT a 75.707,5 e BTC netto scende da $+689 a $+129, con realizzato −22,53. Il 18/09 SKH01 è flat, BTC chiude a $80.907,00 e il realizzato è solo −0,70. Col netting in software, l'uscita dello short è un riacquisto di long e il suo costo non appare in nessun «realizzato». SKH01 è «il meno affidabile dei cinque» (§2) e pesa 25%, ma dal giornale la sua resa live non si misura. | giornali 15-18/09; CLAUDE.md §1, §2 | Dedotto dal meccanismo di netting. |
| 5 | **Giornali 17-20/09 non committati.** Dopo `8007158` non c'è nessun commit. Le pagine 10-15/09 erano già entrate in blocco (`e42ab53`). `docs/journal` non è nell'elenco di backup di §7, e l'Analisi del modello non è ricostruibile (P11). | git status; CLAUDE.md §7 | Misurato. |
| 6 | **§2 invecchiato sul libro live.** «Esposto 14 giorni su 64, max 0,52x» è scritto al giorno 64. Oggi il campione è di 89 giorni e il libro è a mercato in tutte le pagine lette. La riga TWR (+6,66%, +$13,82, lettura 10/09) sta oggi a +8,71% e +$99,15. Il 69% di questi $99,15 cade negli ultimi 7 giorni: è un evento, non un tasso, e non va promosso a «numero da citare». | CLAUDE.md §2; giornale 20/09 | Misurato. |
| 7 | **Contraddizione interna a §1.** Nella parentesi sull'autorità dei parametri si legge «tetto ALLERTA quota 50%», ma nello stesso paragrafo e in config `quota_max_frac` è **0,85** (M28). C'è anche una frase spezzata: «dopo un bonifico si **dal 10/09 lo fa…». | CLAUDE.md §1; config `usde` | Misurato. |
| 8 | **Annuncio Deribit non letto.** `venue_news` ha registrato il 16/09 «Deribit Launches Its Biggest-Ever Product Expansion» con `match: []`. Un'espansione di prodotto può toccare margini, strumenti o collaterale senza contenere le parole chiave. Il debito 8 (Rulebook non sorvegliato) resta aperto. | venue_news.jsonl; CLAUDE.md §5.8 | Dedotto. |
| 9 | **Revisioni saltate o dubbie.** Quella del 14/09 è «NON eseguita, limite Fable», quella del 16/09 ha «esito sospetta». Il testo non è nel materiale, quindi non posso dire se i loro punti siano stati chiusi. | commit `e42ab53` | Non determinabile. |
Sul cuscino: la quota USDE è 0,6333 (usde_watch 20/09), sotto il bersaglio 0,64, e `cuscino_watch` risulta OK in monitor_health. Lo slack corrente non è determinabile, perché il registro di `cuscino_watch` non è nel materiale.
## Cosa e' maturo da decidere
| Gate o scadenza | Data | Criterio scritto | Stato nel materiale |
|---|---|---|---|
| **STATARB** | 27/09 (fra 6 giorni) | «soglie 24/07 (non toccate) + diagnostica statica sempre-short» | Il monitor registra (OK). Lo Sharpe più recente che ho è −1,61 del 26/08. Le soglie numeriche **non stanno in §4**. Non anticipo il verdetto. Da decidere prima: chi lo legge domenica, e da quale documento prende le soglie. |
| **Finestra OMBRA** | 30/09 | Nessun criterio di esito scritto: §1 cita solo lo strumento di confronto. | Esito dei confronti non nel materiale. Senza un criterio dichiarato prima, il 30/09 si decide guardando l'esito, e §4 lo vieta per i gate. Decide l'operatore. |
| XSR01 · kill DVOLSPREAD | 23/10 · 24/10 | §4 | Cadono a 32 e 33 giorni, appena fuori dalla finestra dei 30. Entrambi i monitor sono OK. |
| USDE-01, PROP-01 | passati | — | Chiusi (§4). La frase «`quota_da_decidere()` lo chiede ogni giorno da lunedì» è superata dalla quota decisa il 10/09. |
| `vrp_f_watch` | — | criteri (a) e (b), §5.18 | pairs 30 (erano 28/40 il 09/09), `esclude_zero` true, `ready` false. Nessuna azione: la decisione §3 sul short-vol non è riaperta (IV-rank 0,03 e 0,13 contro 0,30, giornale 20/09). |
Le soglie di capitale ($15k GTAA01, $20k) non sono toccate: il watermark è $4.550,27.
## Proposte (nessuna azione eseguita)
1. **Operatore:** crei la chiave Deribit di sola lettura per l'ombra (§1) e dichiari entro il 30/09 il criterio con cui la finestra si chiude.
2. **Agente:** verifichi che ogni script lanciato da `cron_daily` e dagli altri cron termini davvero, non solo cc01. Valuti anche di portare sotto `monitor_health` i sorveglianti che reggono una decisione (`fee_watch`, `edge_watch`, `scale_watch`), e di aggiornare in §4 lo stato di `edge_watch`.
3. **Agente:** riporti in §4 le soglie STATARB del 24/07, copiate verbatim dalla fonte e senza toccarle, prima del 27/09.
4. **Agente:** apra una issue per ciascuno dei punti 2 e 3 della tabella «Cosa non torna»: l'abbinamento dei round-trip e il capitale nel prompt dell'analista, che andrebbe derivato.
5. **Attribuzione del P&L per sleeve** (registro virtuale TP01/SKH01 accanto al netto). È strumentazione, non una strategia. Prima di proporla va verificato in `docs/memory/40-produzione-e-deploy.md` se è già stata valutata.
6. **Operatore o agente:** legga l'annuncio Deribit del 16/09, così da confermare che non tocchi i perp USDC, i margini X:SM o l'USDE.
7. **Agente:** committi i giornali 17-20/09, chiarisca se il commit è automatico o no, e corregga §1 (50% contro 0,85, frase spezzata) e le due righe «live» di §2.
## Cosa ho letto e cosa mi mancava
**Letto:** CLAUDE.md per intero, `config/live.json`, crontab, git log dal 07/09 e git status, tabella monitor_health, gli stati di scale_watch, vrp_f_watch, usde_watch, usde_convert, balance_watch, venue_news, movimenti_dichiarati e equity_seen, i giornali dal 14/09 al 20/09. Nessun diario della settimana era nel materiale.
**Mi mancava:**
- il testo delle revisioni del 09/09 e del 16/09, per seguire i punti ancora aperti;
- gli ultimi record di `cuscino_watch`, `edge_watch`, `fee_watch` e `venue_watch`;
- lo Sharpe corrente e il numero di barre dei quattro forward-monitor (STATARB soprattutto);
- le soglie STATARB del 24/07;
- l'output di `confronta_giornata.py` dell'ombra;
- il giornale dell'11/09, citato dall'analista;
- l'ora e il prezzo d'uscita dello short SKH01 del 15-18/09;
- l'n corrente di `usde_watch.rendimento()`;
- l'elenco degli script chiamati da `cron_daily`, con il loro codice d'uscita.
---
## 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) | 87,916 | |
| config/live.json (unica autorita' dei guardrail live) | 8,439 | |
| crontab installata (crontab -l) | 1,860 | |
| git log dal 2026-09-07 + stato del working tree | 1,775 | |
| monitor_health (i forward-monitor stanno registrando?) | 717 | |
| stato scale_watch_state.json | 149 | |
| stato scale_history.jsonl (ultime 3) | 259 | |
| stato vrp_f_watch/state.json | 452 | |
| stato usde_watch.jsonl (ultime 3) | 1,399 | |
| stato usde_convert.jsonl (ultime 3) | 9,520 | |
| stato balance_watch.jsonl (ultime 2) | 583 | |
| stato venue_news.jsonl (ultime 5) | 1,571 | |
| stato movimenti_dichiarati.jsonl | 1,101 | |
| stato equity_seen.json (watermark) | 74 | |
| giornale 2026-09-14 | 3,736 | |
| giornale 2026-09-15 | 3,731 | |
| giornale 2026-09-16 | 3,809 | |
| giornale 2026-09-17 | 3,816 | |
| giornale 2026-09-18 | 4,055 | |
| giornale 2026-09-19 | 3,774 | |
| giornale 2026-09-20 | 3,767 | |
+8 -2
View File
@@ -35,10 +35,16 @@ mkdir -p logs
# partire anche quando book_execute fallisce per la stessa ragione. Non blocca (exit 2
# = allarme inviato); l'azione a un allarme e' manuale, vedi src/live/venue_watch.py.
uv run python scripts/live/venue_watch.py --quiet || true
uv run python scripts/live/book_execute.py --execute
# LOCK SUL CONTO (#36): cuscino_watch (cron_cuscino.sh, :53) puo' mandare ordini spot USDE
# sullo stesso conto — separati solo dal tempo, giri fino a 659s misurati. `flock -n` (mai
# `-w`, un SIGTERM a meta' ordine perde la riga di log): se cuscino_watch tiene il lock questo
# giro si SALTA (rc 75) invece di sovrapporsi. Niente kill/timeout sul comando.
flock -n -E 75 logs/.conto.lock uv run python scripts/live/book_execute.py --execute; rc=$?
[ $rc -eq 75 ] && echo " book: SALTATO — conto occupato da un altro giro (logs/.conto.lock)"
# Libro di bordo: materializza in data/live/trades.db (che il backup COPRE) l'ora vera dei
# fill, che altrimenti vive solo in questo log — gitignored, fuori dal backup e ruotabile.
# Va DOPO book_execute: dentro lo stesso blocco vede le righe appena scritte.
# Va DOPO book_execute: dentro lo stesso blocco vede le righe appena scritte. Fuori dal lock:
# e' sola lettura, non tocca il conto.
uv run python scripts/live/trades_db.py --sync --quiet || true
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_book.log 2>&1
+1 -1
View File
@@ -8,7 +8,7 @@
# MINUTO :25, e la scelta NON e' arbitraria:
# :00 cerbero-bite sparava la sua spazzata full-chain (~44 chiamate/s) e saturava il rate limit
# Deribit per-IP — 12.186 risposte 429 in 26 ore, il 96% nel minuto tondo.
# :07 cron_book.sh (esecuzione del book live). Il feed 5m di SKH01 passa da li': e' la cosa che
# :47 cron_book.sh (esecuzione del book live; era :07 fino al 25/08). Il feed 5m di SKH01 passa da li': e' la cosa che
# NON deve trovare l'IP occupato.
# :25 nessun altro job. Il giro dura ~3 minuti a 4 chiamate/s, quindi finisce ben prima del :30.
#
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# Sorveglianza ORARIA del cuscino USDC di regolamento (debito §5.17, issue #3) — v2.0.0+.
# Minuto :53 = DOPO il giro del book (:47, i cui fill si regolano in USDC) e fuori dal minuto
# tondo (rate-limit per-IP), dal :25 (catena), dal :35 (usde) e dal :42 (balance).
# PUO' INVIARE ORDINI: sotto zero di slack lancia usde_convert --esegui (vende USDE), con le
# guardie dichiarate nel docstring di scripts/live/cuscino_watch.py; lo interruttore e' lo
# stesso del libro (execution_enabled in config/live.json).
# LOCK SUL CONTO (#36): cuscino_watch e book_execute (cron_book.sh, :47) sono separati solo dal
# tempo e possono mandare ordini sullo stesso conto — giri fino a 659s sono stati misurati.
# `flock -n` (mai `-w`: un'attesa farebbe scivolare il giro orario) su logs/.conto.lock: se
# book_execute lo tiene, questo giro si SALTA (rc 75) invece di sovrapporsi.
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_cuscino ====="
flock -n -E 75 logs/.conto.lock uv run python scripts/live/cuscino_watch.py --quiet; rc=$?
[ $rc -eq 75 ] && echo " cuscino: SALTATO — conto occupato (logs/.conto.lock)"
# (#30) giro orario dei sorveglianti a max_age<=6h: notifica solo il NUOVO, marcatore in
# data/live/monitor_health_detto.json. Fuori dal lock: e' sola lettura, non tocca il conto.
uv run python scripts/live/monitor_health.py --quiet --solo-orari || true
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_cuscino.log 2>&1
+14
View File
@@ -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
+16 -1
View File
@@ -73,7 +73,8 @@ def invia(con, g: date, voce: dict, analisi: str, stato: str, motivi: list[str],
def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True,
telegram: bool = True) -> str:
voce = J.costruisci(con, g)
J.salva(con, voce) # le sezioni misurate prima, cosi' il modello le legge
if not secco: # --secco: stampa e NON salva (#34) — niente scrittura
J.salva(con, voce) # le sezioni misurate prima, cosi' il modello le legge
pagina = J.rendi_markdown(con, voce)
st = storico(con, g)
prompt = A.costruisci_prompt(pagina, st)
@@ -115,7 +116,21 @@ def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True
return stato
USO = """uso: analista.py [--giorno AAAA-MM-GG] [--modello NOME] [--secco] [--no-telegram] [--quiet]
(nessun flag) scrive l'analisi di OGGI nel campo `analisi` e manda la notifica
--giorno G analizza il giorno G
--modello M modello da usare (default: src.live.analista.MODELLO_DEFAULT)
--secco stampa e NON salva, niente Telegram
--no-telegram salva ma non manda la notifica
--quiet non stampa
⚠️ SPENDE una chiamata al modello e MANDA un messaggio Telegram, salvo --secco/--no-telegram."""
if __name__ == "__main__":
from src.live.cli import valida
valida("analista.py", USO, flag=("--secco", "--no-telegram", "--quiet"),
con_valore=("--giorno", "--modello"))
con = T.connect()
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT),
+11
View File
@@ -85,6 +85,17 @@ def campiona(c: DeribitRead) -> dict:
rec["n_posizioni"] = sum(1 for p in pos if abs(float(p.get("size") or 0)) > 1)
except Exception:
rec["nozionale_lordo"], rec["n_posizioni"] = None, None
# INDICE usde_usdc, ORARIO (aggiunto 2026-09-02, debito §5.15). `usde_watch` lo registra una
# volta al giorno (12:35Z): troppo rado per il classificatore dei movimenti di capitale, che
# confronta due letture di equity a un'ora di distanza e oggi guarda solo BTC/ETH — con il
# 31% dell'equity in USDE, un depeg sarebbe scorporato come un PRELIEVO. Questa e' la serie
# che manca; il cablaggio nel bound viene dopo, quando ci sara' storia. None se non leggibile
# (P5: "non vedo" non e' "il peg tiene").
try:
from src.live.usde import prezzo_indice
rec["usde_usdc"] = prezzo_indice()
except Exception as e:
rec["usde_usdc"], rec["usde_usdc_errore"] = None, f"{type(e).__name__}"
return rec
+137 -31
View File
@@ -10,9 +10,24 @@ DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali):
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
data/live/book_executions.jsonl.
⚠️ CADENZA: SKH01 decide su griglia 230m -> questo script va lanciato ogni ~230 minuti con la feed
fresca all'ultima barra chiusa (NON il cron giornaliero, che mancherebbe gli ingressi). Gli exit di
SKH sono SOFTWARE (latenza fino a fine barra 230m); solo il disaster-SL (-30%) e' on-book.
⚠️ CADENZA: ORARIA — `47 * * * *` in `scripts/cron_book.sh` (il minuto e' spiegato li'). SKH01
decide su griglia 230m, ma il giro e' IDEMPOTENTE (riconcilia al target netto corrente; sotto
`min_order_usd` $5 -> HOLD). Girare piu' fitto della griglia costa solo MICRO-ORDINI di ri-taglia:
il target e' ricalcolato ogni ora sull'equity marcata, e 22 dei 48 ordini live sono |delta|<=$10
a segnale invariato — spiccioli oggi, ma il deadband e' in valuta ASSOLUTA (regola C2: a capitale
maggiore un movimento dello 0,15% lo supera ogni ora). Compra due cose misurate: (1) gli
ingressi/uscite SOFTWARE di SKH01 arrivano con latenza <=1h — CON LA FEED 5m FRESCA: se e' stantia
`livefeed.fresh_5m` ripiega in silenzio sul feed giornaliero (latenza ~1 giorno) e
`skh_feed_max_age_min` 30 ALLERTA, non blocca; (2) il disaster-SL rotolante (-30%, l'unico on-book)
viene CONTROLLATO ogni ora e ri-ancorato solo oltre la tolleranza di `ensure_disaster_sl` (stop a
>5% da quello piazzato, cioe' mark +5,263%/-4,762%, o taglia >10%) — con questa isteresi
(`r0823_sl_anchor.py`): a cadenza 1h in 7-8 anni BTC 0 scatti / ETH 1; a 4h BTC 2; a 24h ETH 5,
con 2 in 30 giorni. Lo stop siede quindi fra -33,5% e -26,5% dal mark corrente (0,665-0,735 x mark),
non a -30% dall'ultima ora: e' rotolante, non un massimo di perdita (CLAUDE.md §1).
🚨 NON "correggere" la cadenza a ~230 minuti: fino al 2026-09-02 questo docstring lo prescriveva,
ed era il docstring a essere sbagliato, non il cron (debito §5.7). `tests/test_book_cadenza.py`
tiene d'accordo docstring, `cron_book.sh` e crontab installata. NON il cron giornaliero: le entrate
di SKH01 le mancherebbe.
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
@@ -31,7 +46,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.book import ScalaNonAutorizzata, book_report
from src.live.execution import DeribitTrader
from src.live.execution import DeribitTrader, RebalanceParziale
from src.live.notifier import notify
from src.live.venue_probe import diagnose, errori_dal_report
@@ -70,6 +85,33 @@ def log_event(rec: dict):
f.write(json.dumps(rec) + "\n")
def _registra_fill(f, *, asset: str, act: str, net: float, bar_ts, tp_frac: float, skh_sign: int,
pos_after: float | None = None):
"""Registra UN fill (print + log_event + notify). SUBITO dopo l'ordine, PRIMA di leggere la
posizione risultante: un ordine eseguito non deve dipendere dalla lettura della posizione
successiva per essere salvato (#23 — un 502 su `position_usd()` dopo un fill riuscito lo
faceva sparire dal log, perche' la lettura veniva PRIMA del giro `for f in fills`).
`pos_after` arriva dal chiamante (#39 — regressione di #23): nel ramo normale di `_run` si
legge SUBITO dopo il rebalance, in un try che isola la lettura dalla registrazione dei fill;
resta None quando la lettura solleva o quando il passo e' un `RebalanceParziale` (a meta')."""
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
# `ts_utc` = ORA VERA del fill. Fino al 2026-08-23 qui c'era la data della
# BARRA di segnale (`r['last_data']`): 19 righe su 19 a 00:00:00, e un trade
# registrato SEI GIORNI prima di essere eseguito (ETH 0.04 del 14/07, scritto
# 08/07). La barra resta, sotto il suo nome: `bar_ts`.
log_event(dict(ts_utc=datetime.now(timezone.utc).isoformat(timespec="seconds"),
bar_ts=str(pd.Timestamp(bar_ts)), asset=asset, action=act,
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
verified=f.verified, notes=f.notes, net_target=net, pos_after=pos_after,
tp_frac=tp_frac, skh_sign=skh_sign))
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
fee=round(f.fee_usdc, 5), net=round(net, 0))
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
det if f.verified else {**det, "notes": f.notes})
def _run():
cfg = load_config()
want_execute = "--execute" in sys.argv[1:]
@@ -114,8 +156,10 @@ def _run():
print(f" ultima barra : {r['last_data']}\n")
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
# (#22) Non e' piu' "SKH forzato flat!": book_execute non manda PIU' ordini basati su
# quel segnale ignoto (modo SOLO PROTEZIONE, sotto, che porta anche questo errore su
# Telegram — qui solo il print, per non mandare due notify per lo stesso guasto).
print(f" ⚠️ SKH FEED ERRORE (SKH non calcolato: nessun ordine, solo protezione): {r['skh_error']}")
# --- FRESCHEZZA del feed 5m usato per il segnale SKH (2026-07-26) --------------------------
# `fresh_5m` ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e
@@ -205,10 +249,27 @@ def _run():
"azione": "rebuild_history.py --asset BTC ETH"})
return
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
if r.get("eq_fallback"): # equity reale non leggibile -> niente sizing sul conto
# (#21) qui solo il print: il notify e' nel blocco SOLO PROTEZIONE sotto, che porta anche
# questo errore su Telegram — un solo avviso per lo stesso guasto, non due.
print(f" ⚠️ EQUITY FALLBACK (nessun ordine, solo protezione): {r['eq_fallback']}")
# --- SOLO PROTEZIONE (#21 equity ignota, #22 SKH ignoto) ------------------------------------
# Principio unico: quando il giro non sa QUANTO vale il conto o QUALE e' il segnale, NON manda
# ordini di ribilancio (dimensionare un ordine su un numero che non si conosce e' peggio che
# non ordinare) -> ma CONTROLLA LO STESSO il disaster-SL di ogni asset: la protezione dipende
# da posizione e mark, non da equity o segnale. Stesso principio che pos_error applica gia'
# bloccando l'esecuzione, qui senza saltare la protezione.
solo_protezione: str | None = r.get("eq_fallback") or r.get("skh_error")
# Calcolato una volta sola: serve sia nell'azione per-asset del ciclo sotto sia nel
# riepilogo finale (#21, #22) — un solo motivo, non due troncamenti che potrebbero divergere.
motivo_breve = (solo_protezione if solo_protezione and len(solo_protezione) <= 60
else (solo_protezione[:60] + "…" if solo_protezione else None))
if solo_protezione and do_execute:
titolo = ("⚠️ BOOK LIVE — equity non leggibile: nessun ordine, solo protezione"
if r.get("eq_fallback") else
"⚠️ BOOK LIVE — SKH non calcolato: nessun ordine, solo protezione")
notify(titolo, {"nota": solo_protezione})
# Salto di equity: un versamento (conferma che il sizing lo ha seguito) oppure un movimento
# NON richiesto (prelievo, perdita anomala). In entrambi i casi va detto subito, e con il
@@ -235,13 +296,18 @@ def _run():
# non deve togliere la rete di sicurezza all'altro.
falliti: list[str] = []
scoperti: list[str] = []
non_verificati: list[str] = []
for a in r["assets"]:
asset, inst = a["asset"], a["instrument"]
net, cur, mark = a["net_target"], a["position_usd"], a["mark"]
sk = a["skh_state"]
sk_txt = "flat" if sk == "flat" else f"{sk['dir']}@{sk.get('entry')}"
order = a["order"]
if order is None:
if solo_protezione:
# (#21, #22) nessun rebalance: l'azione stampata lo dice, non e' "a target" — e'
# "non tocco perche' non so quanto vale il conto / qual e' il segnale".
act = f"HOLD (solo protezione: {motivo_breve})"
elif order is None:
act = "HOLD (a target)"
elif order.get("is_close"):
act = f"CLOSE ${cur:,.0f}"
@@ -253,26 +319,37 @@ def _run():
f"| pos ${cur:+,.0f} -> {act}")
try:
if do_execute and order is not None:
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
newpos = trader.position_usd(inst)
if do_execute and order is not None and not solo_protezione:
# (#18) tetto di apertura derivato dal cap di config del libro. Se cap_per_asset*scala
# o mark sono <=0 solleva ValueError: e' giusto, finisce nell'isolamento per asset
# (except sotto), non deve fermare il giro intero.
trader.set_open_cap(inst, r["cap_per_asset"] * r.get("scala", 1.0), mark)
try:
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
except RebalanceParziale as rp:
# (#23) almeno un fill e' passato prima che il resto del passo sollevasse
# (es. flip: close ok, open KO) -> si registra quel fill SUBITO (non e' un
# fill perso), poi si rilancia la causa nell'handler di isolamento esistente.
for f in rp.fills:
_registra_fill(f, asset=asset, act=act, net=net, bar_ts=r["last_data"],
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"])
raise rp.causa
# newpos si legge SUBITO dopo il rebalance, in un try che ISOLA la lettura dalla
# registrazione dei fill (#39, regressione di #23: dal merge di #23 pos_after era
# sempre None, perche' la lettura restava un side-effect informativo dopo il ciclo
# e il suo risultato non arrivava piu' a `_registra_fill`). Se solleva, i fill si
# registrano comunque, con pos_after=None — il vincolo #23 (un fill non si perde
# per un reconcile mancato) resta: il try isola solo la LETTURA, non la scrittura.
try:
newpos = trader.position_usd(inst)
pos_after = newpos
print(f" reconcile: pos ${newpos:,.0f}")
except Exception as e: # noqa: BLE001 — non e' un fill perso, e' un reconcile mancato
pos_after = None
print(f" reconcile: pos illeggibile ({type(e).__name__})")
for f in fills:
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
# `ts_utc` = ORA VERA del fill. Fino al 2026-08-23 qui c'era la data della
# BARRA di segnale (`r['last_data']`): 19 righe su 19 a 00:00:00, e un trade
# registrato SEI GIORNI prima di essere eseguito (ETH 0.04 del 14/07, scritto
# 08/07). La barra resta, sotto il suo nome: `bar_ts`.
log_event(dict(ts_utc=datetime.now(timezone.utc).isoformat(timespec="seconds"),
bar_ts=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos,
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"]))
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
fee=round(f.fee_usdc, 5), net=round(net, 0), pos_after=round(newpos, 0))
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
det if f.verified else {**det, "notes": f.notes})
print(f" reconcile: pos ${newpos:,.0f}")
_registra_fill(f, asset=asset, act=act, net=net, bar_ts=r["last_data"],
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"], pos_after=pos_after)
if do_execute:
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
stato = ds.get("state")
@@ -293,6 +370,15 @@ def _run():
"amount": round(ds.get("amount") or 0, 4),
"azione": "ripiazzare il bracket a mano, oppure chiudere la posizione",
"notes": str(ds.get("notes"))[:300]})
elif stato == "unknown":
# (#24) posizione o open_orders illeggibili: NON e' "naked" (non si e' tolto
# nulla, ensure_disaster_sl non cancella ne' piazza su dati a meta'). E' un
# giro degradato lo stesso: non si sa se la protezione esistente e' ancora
# coerente con la posizione attuale.
non_verificati.append(asset)
print(f" ⚠️ {asset}: protezione NON VERIFICATA — {ds.get('notes')}")
notify("⚠️ BOOK — disaster-SL non verificabile (lettura posizione/ordini fallita)",
{"asset": asset, "notes": str(ds.get("notes"))[:300]})
except Exception as e: # noqa: BLE001 — isolamento per asset: l'altro deve continuare
d = diagnose(errori_dal_report(r) + [f"{type(e).__name__}: {e}"])
falliti.append(asset)
@@ -312,16 +398,27 @@ def _run():
print(f" 🚨 POSIZIONI SCOPERTE (nessun disaster-SL on-book): {', '.join(scoperti)}")
if falliti:
print(f" 🛑 asset falliti in questo giro: {', '.join(falliti)}")
if non_verificati:
print(f" ⚠️ disaster-SL NON VERIFICATO (lettura posizione/ordini fallita): {', '.join(non_verificati)}")
if not do_execute:
print(" => DRY-RUN: nessun ordine inviato." +
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
elif solo_protezione:
# (#21, #22) non e' "Nessuna azione: conto gia' al target" — il conto potrebbe NON esserci
# per niente al target, semplicemente non lo si sa. Distinto dal ramo HOLD sotto.
print(f" => SOLO PROTEZIONE: nessun ordine di ribilancio ({motivo_breve}), disaster-SL controllato.")
elif all(x.startswith("HOLD") for x in actions):
print(" => Nessuna azione: conto gia' al target netto del book.")
else:
print(" => Esecuzione completata (vedi data/live/book_executions.jsonl).")
# Uscita non-zero se il giro e' stato DEGRADATO: il log del cron deve poterlo contare senza
# rileggere la prosa. Non cambia nulla operativamente (cron_book.sh non ha `set -e`).
if scoperti or falliti:
# (#21, #22) SOLO PROTEZIONE e' degradato anche senza asset falliti/scoperti: il giro non ha
# ribilanciato nulla perche' non sapeva quanto vale il conto o qual era il segnale. Solo se
# il giro era ARMATO: in DRY-RUN non si e' comunque tentato nulla, e' solo un'anteprima.
# (#24) `non_verificati` e' degradato allo stesso modo: la protezione esistente non e' stata
# controllata, indipendentemente da solo_protezione.
if scoperti or falliti or non_verificati or (solo_protezione and do_execute):
sys.exit(2)
@@ -340,5 +437,14 @@ def main():
raise
USO = """uso: book_execute.py [--execute]
uv run python scripts/live/book_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/book_execute.py --execute # esegue SOLO se execution_enabled=true
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("book_execute.py", USO, flag=("--execute",), con_valore=())
main()
+31 -1
View File
@@ -13,16 +13,29 @@ Hyperliquid (tokenless, stessa fonte certificata di fetch_hl_funding.py) e stamp
che resta comunque vincolato a ~$20k+ di capitale e venue con funding eseguibile).
Uso: `uv run python scripts/live/cc01_regime_watch.py`
(candidato a una riga in cron_daily.sh: e' read-only, nessun ordine, nessuno stato.)
(in cron_daily.sh: read-only verso il mercato, nessun ordine.)
REGISTRA OGNI GIRO MISURATO (dal 2026-09-16, #12) in `data/cc01_regime/runs.jsonl`: `ts` in ms,
stato, funding 30g/7g per asset. Serve a `monitor_health`, che ne legge il percorso da `RUNS`
(P1). Dal 03/09 al 16/09 lo script e' morto 14 notti di fila (#10) e nessuno l'ha visto, perche'
non lasciava niente da sorvegliare. Un giro che non misura entrambi gli asset NON scrive: non e'
una barra, e se lo fosse un cc01 cieco resterebbe «OK».
"""
from __future__ import annotations
import datetime as dt
import json
import sys
import time
from pathlib import Path
import numpy as np
import requests
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
RUNS = ROOT / "data" / "cc01_regime" / "runs.jsonl"
HL_INFO = "https://api.hyperliquid.xyz/info"
HOURS_PER_YEAR = 24 * 365
WARN_ANN = 0.10 # >=10% ann. su media 30g -> WARN
@@ -70,6 +83,7 @@ def main() -> None:
print(" CC01 REGIME WATCH — funding medio 1-mese (Hyperliquid, orario, tokenless)")
print("=" * 78)
worst = "QUIET"
misure = {}
for coin in ("BTC", "ETH"):
f = fetch_recent_funding(coin)
if len(f) < 24 * 7:
@@ -81,8 +95,16 @@ def main() -> None:
"WARN" if ann30 >= WARN_ANN else "QUIET")
if state == "ALERT" or (state == "WARN" and worst == "QUIET"):
worst = state
misure[coin] = {"ann30": ann30, "ann7": ann7, "stato": state}
print(f" {coin}: media 30g {ann30:+7.1%} ann. media 7g {ann7:+7.1%} ann. -> {state}")
print("-" * 78)
if len(misure) < 2:
# P5: «non vedo» non e' «QUIET». Nessuna riga: monitor_health lo vedra' FERMO.
print(f" NON VEDO: misurati {len(misure)} asset su 2 — nessuna riga registrata in {RUNS.name}.")
return
RUNS.parent.mkdir(parents=True, exist_ok=True)
with RUNS.open("a", encoding="utf-8") as fh:
fh.write(json.dumps({"ts": int(time.time() * 1000), "stato": worst, **misure}) + "\n")
if worst == "ALERT":
print(" >>> ALERT CC01: funding 30g >= 15% ann. — regime ricco SOSTENUTO.")
print(" Riaprire il dossier cash-and-carry (vincoli invariati: ~$20k+,")
@@ -93,5 +115,13 @@ def main() -> None:
print(" regime QUIET: carry non raccoglibile (coerente con luglio 2026).")
USO = """uso: cc01_regime_watch.py
(nessun flag) sorveglia il regime di CC01 e stampa lo stato.
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("cc01_regime_watch.py", USO)
main()
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/env python
"""cuscino_watch.py — sorveglianza ORARIA del cuscino USDC di regolamento. PUO' INVIARE ORDINI.
PERCHE' ESISTE (debito §5.17, issue #3). Dal 06/09 il 68,7% dell'equity e' USDE. P&L e funding dei
perp USDC-lineari si regolano in USDC: una perdita del libro consuma il cuscino e fa salire la quota
USDE DA SOLA, senza un ordine. `usde_watch` allerta solo su `quota > quota_max_frac` (0,85): a
68,7% lo slack sul cuscino era ~$41-58, e un saldo USDC negativo costa lo 0,05%/GIORNO (18,25%/anno,
4,3x la resa che l'USDE compra). Nessuno guardava la grandezza giusta.
LA MISURA (decisione dell'operatore, 10/09): l'EQUITY USDC del conto — balance piu' P&L non
realizzato — perche' e' quella che risponde del regolamento; il balance vedrebbe il buco solo a
trade chiuso. Il cuscino richiesto e' DERIVATO da config/live.json e src/live/book.py tramite
`usde.cuscino_richiesto_usd` (n_asset x frac x disaster_sl_pct x equity totale): la stessa formula
che `usde_convert` applica a ogni piano — un sorvegliante che ridichiara il proprio bersaglio non
sta controllando niente (P1, 5 occorrenze nel progetto).
QUATTRO STATI SULLO SLACK (= equity USDC − cuscino), in frazioni del cuscino lette da config
(`usde.bande_cuscino`: preavviso 0,10 · margine 0,20 · riacquisto 0,40 — P5, P1):
* SCOPERTO — slack < 0: 🚨 alla transizione, e VENDITA automatica di USDE fino alla quota di
ripristino q* = usde.quota_ripristino() = 1 − 0,30 × 1,20 = 0,64 (cuscino + 20%).
* PREAVVISO — 0 <= slack < 10% del cuscino: ⚠️ alla transizione. Latenza comprata apposta.
* OK — 10% <= slack <= 40%: niente.
* ECCEDENTE — slack > 40% del cuscino: ACQUISTO automatico di USDE fino allo stesso q* (issue #7,
decisione dell'operatore 10/09: «riportarla a quota in autonomia»). E' cio' che
riporta la quota a bersaglio dopo un bonifico (+0,7 × importo di slack) o dopo un
tratto di guadagni del libro, senza che nessuno se ne ricordi. 📌 alla transizione.
* BLIND — conto non leggibile: ⚠️ alla transizione. "Non vedo" non e' "va tutto bene".
ISTERESI: vendita sotto 0, acquisto sopra 0,40, entrambe verso 0,20. Per oscillare lo slack deve
muoversi di ±0,20 × cuscino = ±6% dell'equity, e siccome slack = 0,7·USDC − 0,3·USDE, servono
±8,6% di equity USDC (~$380 oggi); un giro costa ~6 bps sull'importo mosso. Soglie DICHIARATE, non ottimizzate (M8). Il vecchio `quota_target` 0,70
lasciava slack ZERO per costruzione: a quella quota ogni ora in perdita avrebbe venduto.
LE GUARDIE DELLA RICONVERSIONE (tutte dichiarate qui):
* `execution_enabled` in config/live.json deve essere true — lo stesso interruttore del libro:
disarmare il libro disarma anche questo, senza un secondo posto da ricordare.
* NON si converte sotto `depeg_warn`, in nessun verso: vendere USDE sotto la pari cristallizza
la perdita del collaterale, comprarne mentre scivola e' peggio; li' decide l'operatore.
* al massimo UN tentativo ogni RIPRISTINO_MIN_ORE (6h), in qualunque verso: un tentativo che si
ripete ogni ora e' spam, e un sorvegliante che si ripete si impara a ignorare (P14).
* dopo un tentativo FALLITO da ECCEDENTE si riprova dopo RITENTATIVO_FALLITO_ORE (24h): USDC
fermo non e' un rischio, e un guasto persistente (tetto del venue tornato, book illeggibile)
diventerebbe 4 Telegram al giorno per sempre (revisione fable 10/09). Da SCOPERTO il ritmo
resta 6h: li' il fallimento e' un rischio che costa 0,05%/giorno.
* `usde_convert` porta le proprie guardie (banda di prezzo, book leggibile, tetto HARD sulla
quota): questo script NON le duplica, le eredita lanciando lo script vero (P15: la regola si
prova contro il codice che la esegue).
* `--secco`: niente ordini, niente Telegram, NIENTE riga nel registro: stampa cosa FAREBBE.
* niente lock: un `usde_convert --esegui` lanciato a mano nello stesso minuto del cron (:53)
puo' vendere due volte verso bersagli diversi. Limite dichiarato (D5), non riparato.
TRASPORTO (lezione del debito §5.2): il marcatore "gia' detto" e' l'esito di `notify` scritto nel
record (`allertato`); se l'invio fallisce la transizione si ripete al giro dopo invece di andare
persa per l'episodio intero.
La serie data/live/cuscino_watch.jsonl e' dentro il perimetro di backup ed e' sorvegliata da
monitor_health (un watch fermo = nessuno guarda il cuscino, e il silenzio si legge come zero).
uv run python scripts/live/cuscino_watch.py # un giro: legge, giudica, agisce
uv run python scripts/live/cuscino_watch.py --quiet # stampa solo se cambia qualcosa
uv run python scripts/live/cuscino_watch.py --secco # nessun ordine, nessun Telegram
"""
from __future__ import annotations
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live import usde as U # noqa: E402
from src.live.notifier import notify # noqa: E402
STATE = ROOT / "data" / "live" / "cuscino_watch.jsonl"
CONVERT = ROOT / "scripts" / "live" / "usde_convert.py"
# Le bande (preavviso / margine / riacquisto) NON stanno qui: si leggono da config via
# usde.bande_cuscino() a ogni giro (P1). Il margine e' il DOPPIO del preavviso per costruzione
# dell'ordine imposto (revisione fable 10/09: a margine == preavviso ogni 🚨 era seguito da un ⚠️).
RIPRISTINO_MIN_ORE = 6.0 # un tentativo di conversione (in qualunque verso) ogni 6 ore
RITENTATIVO_FALLITO_ORE = 24.0 # da ECCEDENTE, dopo un fallimento: un tentativo al giorno
TIMEOUT_CONVERT_S = 900 # usde_convert: 12s di pausa fra ordini + le GET per giro (~14s/giro
# coi 12s di pausa); 60 ordini ~= 840s: il tetto e' stretto sul caso
# massimo, non largo come "60 ordini = 12 min" lasciava credere (#35 B10)
def leggi(path: Path = STATE) -> list[dict]:
if not path.exists():
return []
out = []
for ln in path.read_text().splitlines():
ln = ln.strip()
if ln:
out.append(json.loads(ln))
return out
def giudica(eq_usdc: float | None, equity_tot: float | None, cuscino: float | None,
bande: dict | None = None) -> dict:
"""PURA. -> stato, slack, soglie in dollari. `None` in ingresso = BLIND. `bande` = le frazioni
del cuscino (default: config, via usde.bande_cuscino)."""
if eq_usdc is None or equity_tot is None or cuscino is None:
return dict(stato="BLIND", slack=None, preavviso_usd=None, riacquisto_usd=None)
try:
b = bande or U.bande_cuscino()
except Exception as e: # noqa: BLE001 — config rotta = "non vedo", non un crash
return dict(stato="BLIND", slack=None, preavviso_usd=None, riacquisto_usd=None,
motivo=f"bande del cuscino non leggibili: {e}")
slack = eq_usdc - cuscino
preavviso, riacquisto = b["preavviso"] * cuscino, b["riacquisto"] * cuscino
if slack < 0:
stato = "SCOPERTO"
elif slack < preavviso:
stato = "PREAVVISO"
elif slack > riacquisto:
stato = "ECCEDENTE"
else:
stato = "OK"
return dict(stato=stato, slack=round(slack, 2), preavviso_usd=round(preavviso, 2),
riacquisto_usd=round(riacquisto, 2))
AZIONE = {"SCOPERTO": "vendita", "ECCEDENTE": "acquisto"} # gli stati che convertono
def transizione(prev: dict | None, stato: str) -> bool:
"""PURA. Si allerta se lo stato e' cambiato, O se l'ultima allerta per questo stato non e'
partita (`allertato` False): un invio fallito non consuma la transizione (debito §5.2)."""
if stato == "OK":
return False
if prev is None or prev.get("stato") != stato:
return True
return not bool(prev.get("allertato"))
def puo_riconvertire(prev_records: list[dict], now_ms: int, execution_enabled: bool,
px: float | None, depeg_warn: float,
min_ore: float = RIPRISTINO_MIN_ORE, stato: str = "SCOPERTO",
dopo_fallito_ore: float = RITENTATIVO_FALLITO_ORE) -> tuple[bool, str]:
"""PURA. Le guardie della conversione automatica. -> (si', motivo se no). ⚠️ il motivo finisce
in un'allerta: niente '<' (Telegram HTML) — l'escape nel sink lo copre, ma qui non serve."""
if not execution_enabled:
return False, "execution_enabled=false in config/live.json (il libro e' disarmato: anche questo)"
if px is None:
return False, "prezzo USDE non leggibile: non si vende al buio (P5)"
if px < depeg_warn:
return False, f"USDE {px:.4f} sotto depeg_warn {depeg_warn}: ne' vendere (cristallizza) ne' comprare (scivola) — decide l'operatore"
# contano solo i tentativi VERI (subprocess lanciato): un record con `tentata=False` — prezzo
# illeggibile, --secco, interruttore spento — non consuma il budget (revisione fable 10/09)
ultimo = [r for r in prev_records if (r.get("riconversione") or {}).get("tentata")]
if ultimo:
ore = (now_ms - int(ultimo[-1]["ts"])) / 3_600_000
fallito = not (ultimo[-1].get("riconversione") or {}).get("ok")
soglia = dopo_fallito_ore if (fallito and stato == "ECCEDENTE") else min_ore
if ore < soglia:
return False, (f"ultimo tentativo {ore:.1f}h fa, sotto le {soglia:.0f}h"
+ (" (era FALLITO: da ECCEDENTE si riprova una volta al giorno)" if soglia == dopo_fallito_ore else "")
+ ": non si insiste")
return True, ""
def _leggi_quota_usde() -> float | None:
"""Quota USDE sul totale, STESSA formula di usde_watch.py:275 (usd/(usd+eq_usdc)), stessi
mattoni di main() qui sotto (_safe_client, account_summary, U.prezzo, U.valuta): nessuna
lettura nuova. None se il conto non e' leggibile — chi chiama tratta None come "non lo so",
non come zero (#35 B10)."""
client = _safe_client()
if client is None:
return None
try:
eq_usdc = float(client.account_summary("USDC")["equity"])
eq_usde = float(client.account_summary("USDE").get("equity") or 0)
except Exception:
return None
px, _ = U.prezzo(client)
usd, _ = U.valuta(eq_usde, px)
try:
return round(usd / (usd + eq_usdc), 4)
except ZeroDivisionError:
return None
def riconverti(quota: float, timeout_s: float = TIMEOUT_CONVERT_S) -> dict:
"""Lancia lo script VERO con le sue guardie. Ritorna esito + coda dell'output (P3).
Rilegge la quota USDE prima e dopo il subprocess, ANCHE su timeout o eccezione: un rc!=0 con
la quota mossa e' una conversione PARZIALE (fill gia' avvenuti prima del kill a timeout_s),
non "FALLITA" come diceva `ok = rc==0` (#35 B10). `esito` in "ok" | "parziale" | "fallita"* —
"ok" solo per compatibilita' resta anche in `ok` (bool)."""
quota_prima = _leggi_quota_usde()
cmd = [sys.executable, str(CONVERT), "--quota", f"{quota:.4f}", "--esegui"]
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout_s, cwd=str(ROOT))
rc = r.returncode
coda = (r.stdout + r.stderr).strip().splitlines()[-12:]
except subprocess.TimeoutExpired:
rc, coda = None, [f"timeout dopo {timeout_s:.0f}s"]
except Exception as e: # noqa: BLE001 — si registra, non si ingoia
rc, coda = None, [f"{type(e).__name__}: {e}"]
quota_dopo = _leggi_quota_usde()
if rc == 0:
esito = "ok"
elif quota_dopo is None:
esito = "fallita (quota dopo non leggibile)"
else:
mossa = quota_prima is not None and abs(quota_dopo - quota_prima) > 0.005
esito = "parziale" if mossa else "fallita"
return dict(rc=rc, ok=esito == "ok", esito=esito, quota_prima=quota_prima, quota_dopo=quota_dopo,
coda=coda, cmd=" ".join(cmd[1:]))
def _safe_client():
try:
from src.live.deribit import DeribitRead
return DeribitRead()
except Exception:
return None
def _execution_enabled() -> bool:
try:
return bool(json.loads((ROOT / "config" / "live.json").read_text()).get("execution_enabled"))
except Exception:
return False
def main() -> int:
quiet, secco = "--quiet" in sys.argv, "--secco" in sys.argv
c = U.cfg()
records = leggi()
prev = records[-1] if records else None
now = datetime.now(timezone.utc)
now_ms = int(now.timestamp() * 1000)
client = _safe_client()
eq_usdc = eq_usde = None
motivo_blind = None
if client is None:
motivo_blind = "gateway non raggiungibile"
else:
try:
eq_usdc = float(client.account_summary("USDC")["equity"])
except Exception as e:
motivo_blind = f"conto USDC non leggibile ({type(e).__name__})"
try:
eq_usde = float(client.account_summary("USDE").get("equity") or 0)
except Exception as e:
motivo_blind = motivo_blind or f"conto USDE non leggibile ({type(e).__name__})"
px, px_fonte = U.prezzo(client)
equity_tot = cuscino = come = None
if eq_usdc is not None and eq_usde is not None:
usd, _ = U.valuta(eq_usde, px)
equity_tot = eq_usdc + usd
cuscino, come = U.cuscino_richiesto_usd(equity_tot)
try:
bande = U.bande_cuscino(c)
except Exception as e: # noqa: BLE001 — si registra e si allerta, non si crasha (P5)
bande, motivo_blind = None, f"config usde: {e}"
g = giudica(eq_usdc, equity_tot, cuscino, bande)
stato = g["stato"]
if stato == "BLIND" and g.get("motivo"):
motivo_blind = motivo_blind or g["motivo"]
rec = dict(ts=now_ms, data=now.strftime("%Y-%m-%dT%H:%M:%SZ"), stato=stato,
motivo_blind=motivo_blind, eq_usdc=eq_usdc, eq_usde=eq_usde, px=px,
px_fonte=px_fonte, equity_tot=equity_tot, cuscino=cuscino, cuscino_come=come,
slack=g["slack"], preavviso_usd=g["preavviso_usd"], riacquisto_usd=g["riacquisto_usd"],
bande=bande, allertato=None, allerta_errore=None, riconversione=None, secco=secco)
# --- conversione automatica: SCOPERTO vende, ECCEDENTE compra, stesso bersaglio q* ---
if stato in AZIONE:
ok, perche = puo_riconvertire(records, now_ms, _execution_enabled(), px, c["depeg_warn"], stato=stato)
quota = U.quota_ripristino(bande["margine"])
if not ok:
rec["riconversione"] = dict(tentata=False, verso=AZIONE[stato], quota=quota, motivo=perche)
elif secco:
rec["riconversione"] = dict(tentata=False, verso=AZIONE[stato], quota=quota,
motivo=f"--secco: avrei lanciato usde_convert --quota {quota:.4f} --esegui ({AZIONE[stato]})")
else:
ris = riconverti(quota)
rec["riconversione"] = dict(tentata=True, verso=AZIONE[stato], quota=quota, **ris)
# --- allarmi: alla transizione, marcatore scritto con l'esito dell'invio ---
# ⚠️ Telegram e' in parse_mode HTML: un '<' nudo nel testo fa fallire l'invio (successo al
# primo giro vero, 13:53Z del 10/09: «(< 10% del cuscino)»). Qui non si scrive mai '<'.
def _esito_azione(r: dict) -> str:
if not r.get("tentata"):
return f"{r.get('verso', 'conversione')} NON tentata: {r.get('motivo')}"
esito = r.get("esito", "ok" if r.get("ok") else "fallita")
if esito == "parziale":
qp, qd = r.get("quota_prima"), r.get("quota_dopo")
qp_s = f"{qp:.0%}" if qp is not None else "n/d"
qd_s = f"{qd:.0%}" if qd is not None else "n/d"
return f"{r['verso']} PARZIALE: quota da {qp_s} a {qd_s}"
return (f"{r['verso']} " + ("ESEGUITA" if esito == "ok" else "FALLITA")
+ f" (quota bersaglio {r['quota']:.0%})")
if transizione(prev, stato):
if secco:
rec["allertato"] = False
elif stato == "SCOPERTO":
rec["allertato"] = notify("🚨 CUSCINO USDC SCOPERTO", {
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f} ({come})",
"slack": f"${g['slack']:+,.2f}", "azione": _esito_azione(rec["riconversione"] or {}),
"nota": "un USDC negativo costa 0,05%/giorno; il libro si regola in USDC"}, tentativi=3)
elif stato == "PREAVVISO":
rec["allertato"] = notify("⚠️ cuscino USDC in esaurimento", {
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f}",
"slack": f"${g['slack']:+,.2f} (sotto il {bande['preavviso']:.0%} del cuscino)",
"nota": f"sotto zero vende USDE da solo fino a quota {U.quota_ripristino(bande['margine']):.0%}"})
elif stato == "ECCEDENTE":
rec["allertato"] = notify("📌 cuscino USDC eccedente: riacquisto USDE", {
"USDC equity": f"${eq_usdc:,.2f}", "cuscino richiesto": f"${cuscino:,.2f}",
"slack": f"${g['slack']:+,.2f} (sopra il {bande['riacquisto']:.0%} del cuscino)",
"azione": _esito_azione(rec["riconversione"] or {})})
elif stato == "BLIND":
rec["allertato"] = notify("⚠️ cuscino_watch BLIND", {"motivo": motivo_blind or "?",
"nota": "'non vedo' non e' 'va tutto bene' (P5)"})
elif rec["riconversione"] and rec["riconversione"].get("tentata") and not secco:
# non e' una transizione ma e' un ordine mandato: si dice sempre
r = rec["riconversione"]
esito = r.get("esito", "ok" if r.get("ok") else "fallita")
verbo = {"ok": "eseguita", "parziale": "PARZIALE"}.get(esito, "FALLITA")
rec["allertato"] = notify(f"📌 cuscino USDC: {r['verso']} {verbo}",
{"quota bersaglio": f"{r['quota']:.0%}", "esito": " | ".join(r["coda"][-3:])})
if rec["allertato"] is False and not secco:
from src.live.notifier import ultimo_errore
rec["allerta_errore"] = ultimo_errore() # P3: si registra dove si ingoia
# --secco e' secco ANCHE sul registro: un giro a mano non deve diventare il `prev` del cron,
# ne' far sembrare vivo un monitor fermo (revisione fable 10/09)
if not secco:
STATE.parent.mkdir(parents=True, exist_ok=True)
with STATE.open("a") as fh:
fh.write(json.dumps(rec) + "\n")
cambiato = prev is None or prev.get("stato") != stato or rec["riconversione"] is not None
if not quiet or cambiato:
print("=" * 78)
print(f" CUSCINO WATCH — {rec['data']} {'(SECCO)' if secco else ''}")
print("=" * 78)
if stato == "BLIND":
print(f" stato : BLIND — {motivo_blind}")
else:
print(f" conto : USDC equity ${eq_usdc:,.2f} + USDE {eq_usde:,.2f} @ {px if px else 'n/d'}"
f" ({px_fonte}) = ${equity_tot:,.2f}")
print(f" cuscino : ${cuscino:,.2f} richiesti ({come})")
print(f" slack : ${g['slack']:+,.2f} (preavviso sotto ${g['preavviso_usd']:,.2f},"
f" riacquisto sopra ${g['riacquisto_usd']:,.2f}, bersaglio quota {U.quota_ripristino(bande['margine']):.0%})")
print(f" stato : {stato}")
if rec["riconversione"]:
r = rec["riconversione"]
print(f" {r.get('verso', 'conversione'):<12}: {'TENTATA' if r.get('tentata') else 'NON tentata'} — quota {r['quota']:.4f}"
+ (f" — {r.get('motivo')}" if r.get("motivo") else ""))
for ln in r.get("coda", []):
print(f" {ln}")
if rec["allertato"] is not None:
print(f" allerta : {'inviata' if rec['allertato'] else 'NON inviata (si ripete al giro dopo)'}"
+ (f" — {rec['allerta_errore']}" if rec.get("allerta_errore") else ""))
return 0
USO = """uso: cuscino_watch.py [--quiet] [--secco]
cuscino_watch.py — sorveglianza ORARIA del cuscino USDC; sotto zero vende USDE, sopra la banda ricompra.
uv run python scripts/live/cuscino_watch.py # un giro: legge, giudica, agisce
uv run python scripts/live/cuscino_watch.py --quiet # stampa solo se cambia qualcosa
uv run python scripts/live/cuscino_watch.py --secco # nessun ordine, nessun Telegram
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("cuscino_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
sys.exit(main())
+15 -1
View File
@@ -42,10 +42,13 @@ import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify # noqa: E402
from src.portfolio.portfolio import combine_outer # noqa: E402
from src.portfolio.sleeves import deribit_book_sleeves, tp01_sleeve # noqa: E402
RUNS = ROOT / "data" / "edge_watch" / "runs.jsonl"
# --- soglie CONGELATE (cambiarle invalida la taratura di r0726_edge_death.py) ---
WIN_MONTHS = 36
SHARPE_KILL = -0.5
@@ -119,6 +122,7 @@ def run() -> dict:
def main() -> int:
r = run()
registra_giro(RUNS, sharpe=r["sharpe"], n_alert=len(r["alerts"]))
if "--quiet" not in sys.argv:
print("=" * 78)
print(" EDGE WATCH — il book live sta ancora funzionando?")
@@ -136,7 +140,7 @@ def main() -> int:
print(f"\n {n_ok}/{len(r['protection'])} anni di sinistro superati · "
f"fallimenti consecutivi in corso: {r['consec_fail']}")
if r["alerts"]:
notify("🚨 EDGE WATCH — criterio di kill scattato sul book LIVE",
notify("🚨 EDGE WATCH — criterio di kill scattato sul book (serie backtest dei sleeve, non il P&L eseguito)",
{f"alert {i+1}": a for i, a in enumerate(r["alerts"])})
for a in r["alerts"]:
print(f" ALERT: {a}")
@@ -144,5 +148,15 @@ def main() -> int:
return 0
USO = """uso: edge_watch.py [--quiet]
edge_watch.py — sorveglianza dell'EDGE del book live. Riporta e allerta; non tocca nulla.
uv run python scripts/live/edge_watch.py # report
uv run python scripts/live/edge_watch.py --quiet # solo se qualcosa scatta (cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("edge_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main())
+68 -7
View File
@@ -49,9 +49,11 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.book import INSTRUMENT as BOOK_INSTRUMENT # noqa: E402
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify # noqa: E402
STATE = ROOT / "data" / "fee_watch" / "state.json"
RUNS = ROOT / "data" / "fee_watch" / "runs.jsonl"
# ⚠️ CORREZIONE 2026-08-21. Qui c'era la tupla CABLATA ("BTC-PERPETUAL", "ETH-PERPETUAL") — gli
# INVERSE, regolati in BTC/ETH — mentre il book esegue sui LINEARI USDC (BTC_USDC-PERPETUAL).
# Sono due linee di prodotto distinte e Deribit le cambia in modo indipendente: il 18/08 ha
@@ -77,6 +79,7 @@ SOGLIA_OK = 5.0 # <= -> nessuna azione
SOGLIA_AZIONE = 10.0 # > -> rivedere il peso di SKH01
D_SHARPE_PER_BPS = -0.017 # sensibilita' marginale del BOOK, misurata
D_CAGR_PER_BPS = -0.0023
SOGLIA_DIVERGENZA_BPS = 1.0 # #37 B9: |fee realizzata - tier pubblico| oltre questo -> notify
def fetch_fees(instrument: str) -> dict:
@@ -207,14 +210,30 @@ def run() -> dict:
changes = diff_vs(prev, cur)
worst = max(c["taker_bps"] for c in cur.values())
lvl, why = verdict(worst)
# issue #37 (B9). realized_fee_bps() girava SOLO dentro il blocco di stampa: in cron
# (--quiet, il caso normale) non girava mai e una fee realmente pagata diversa dal tier
# pubblico non avvisava nessuno. Qui si calcola SEMPRE ed entra nel record del giro
# (registra_giro, sotto). {} -> None: "non misurata" (nessun trade recente), mai "zero".
real = realized_fee_bps() or None
divergenze = []
if real:
for ins, bps in real.items():
tier = cur.get(ins, {}).get("taker_bps")
if tier is None:
continue
d = bps - tier
if abs(d) > SOGLIA_DIVERGENZA_BPS:
divergenze.append(
f"{ins}: realizzata {bps:.2f} bps vs tier pubblico {tier:.2f} ({d:+.2f})")
return dict(current=cur, changes=changes, worst_taker_bps=worst, level=lvl, reason=why,
first_read=not prev)
first_read=not prev, realized=real, divergenze=divergenze,
divergenza_gia_detta=bool(prev.get("divergenza_detta")))
def main() -> int:
r = run()
quiet = "--quiet" in sys.argv
interessante = bool(r["changes"]) or r["level"] != "OK"
interessante = bool(r["changes"]) or r["level"] != "OK" or bool(r.get("divergenze"))
if not quiet or interessante:
print("=" * 86)
@@ -230,7 +249,7 @@ def main() -> int:
print(" (prima lettura: stato inizializzato, nessun confronto possibile)")
for c in r["changes"]:
print(f" CAMBIO: {c}")
real = realized_fee_bps()
real = r.get("realized") # calcolato una volta in run(), non piu' qui (#37 B9)
if real:
print("\n cross-check sui trade REALI del conto (fonte autorevole):")
for ins, bps in real.items():
@@ -238,19 +257,61 @@ def main() -> int:
nota = ""
if tier is not None:
d = bps - tier
nota = f" (tier taker {tier:.2f} → {d:+.2f})" + (" ⚠️ DIVERGE" if abs(d) > 1.0 else "")
nota = (f" (tier taker {tier:.2f} → {d:+.2f})"
+ (" ⚠️ DIVERGE" if abs(d) > SOGLIA_DIVERGENZA_BPS else ""))
print(f" {ins:<22}{bps:>9.2f} bps/lato effettivi{nota}")
else:
print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)")
if r["changes"] or r["level"] == "AZIONE":
notify("💸 FEE WATCH — schema fee Deribit", {
# issue #29 (B8): save_state veniva chiamato SEMPRE, anche se l'invio necessario falliva —
# il giro dopo `diff_vs` non avrebbe piu' visto il cambiamento (lo stato salvato era gia'
# quello nuovo) e il cambio andava perso per sempre invece che ritentare al giro dopo.
serve_invio = bool(r["changes"] or r["level"] == "AZIONE")
inviato = True
if serve_invio:
inviato = notify("💸 FEE WATCH — schema fee Deribit", {
"verdetto": f"[{r['level']}] {r['reason']}",
**{f"cambio {i+1}": c for i, c in enumerate(r["changes"])},
})
save_state(r["current"])
if not inviato and (not quiet or interessante):
print(" ⚠️ invio FALLITO — stato NON salvato: il prossimo giro ritenta")
# issue #37 (B9): fee realmente pagata che diverge dal tier pubblico atteso -> notify UNA
# volta per episodio. Marcatore "divergenza_detta" nello stesso state.json (stessa regola di
# B8: si scrive solo se l'invio e' riuscito), azzerato quando la divergenza torna sotto
# soglia. E' un evento indipendente dal cambio di tier: se qui serve un invio e quello di B8
# e' appena fallito, save_state sotto non gira (stessa gate di B8) e il marcatore non si
# scrive — al giro dopo si ridice, ridondante ma mai persa, la stessa scelta di B8.
divergenze = r.get("divergenze") or []
gia_detta = bool(r.get("divergenza_gia_detta"))
marcatore = gia_detta
if divergenze and not gia_detta:
inviata_div = notify("💸 FEE WATCH — fee realizzata diverge dal tier pubblico", {
f"divergenza {i+1}": d for i, d in enumerate(divergenze)
})
if inviata_div:
marcatore = True
elif not quiet or interessante:
print(" ⚠️ invio divergenza FALLITO — marcatore non scritto: il prossimo giro ritenta")
elif not divergenze:
marcatore = False
stato_da_salvare = dict(r["current"])
stato_da_salvare["divergenza_detta"] = marcatore
if not serve_invio or inviato:
save_state(stato_da_salvare)
registra_giro(RUNS, level=r["level"], realized=r.get("realized"))
return 2 if r["level"] == "AZIONE" else 0
USO = """uso: fee_watch.py [--quiet]
fee_watch.py — sorveglia lo schema fee di Deribit e applica la regola DECISA IN ANTICIPO.
uv run python scripts/live/fee_watch.py # report
uv run python scripts/live/fee_watch.py --quiet # stampa/allerta solo se qualcosa cambia
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("fee_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main())
+12
View File
@@ -41,7 +41,19 @@ def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path:
return f
USO = """uso: journal.py [--giorno AAAA-MM-GG] [--backfill N] [--nota "testo"] [--quiet]
(nessun flag) scrive la voce di OGGI (pagina marcata PARZIALE: il giorno non e' chiuso)
--giorno G scrive la voce del giorno G
--backfill N ricostruisce gli ultimi N giorni, dal piu' vecchio
--nota "..." annota il giorno (campo dell'operatore, mai riscritto da un ricalcolo)
--quiet non stampa la riga di riepilogo
⚠️ SCRIVE: una riga nel DB e una pagina in docs/journal/. Sola lettura su feed, log e venue."""
if __name__ == "__main__":
from src.live.cli import valida
valida("journal.py", USO, flag=("--quiet",), con_valore=("--giorno", "--backfill", "--nota"))
# sincronizzare PRIMA: altrimenti la sezione 'Libro' (letta dal log) e la sezione 'P&L'
# (letta dal DB) descrivono due istanti diversi e la pagina si contraddice da sola.
sys.path.insert(0, str(ROOT / "scripts" / "live"))
+9
View File
@@ -137,5 +137,14 @@ def main():
raise
USO = """uso: live_execute.py [--execute]
uv run python scripts/live/live_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/live_execute.py --execute # esegue SOLO se execution_enabled=true
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("live_execute.py", USO, flag=("--execute",), con_valore=())
main()
+10
View File
@@ -74,5 +74,15 @@ def main():
(f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini."))
USO = """uso: live_trend.py [--equity V] [--no-net]
uv run python scripts/live/live_trend.py # shadow su mainnet reale
uv run python scripts/live/live_trend.py --equity 2000 # forza la base di sizing
uv run python scripts/live/live_trend.py --no-net # offline: solo matematica + parita'
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("live_trend.py", USO, flag=("--no-net",), con_valore=("--equity",))
main()
+9
View File
@@ -88,5 +88,14 @@ def main():
print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.")
USO = """uso: microtest.py [--live]
uv run python scripts/live/microtest.py # DRY-RUN: nessun ordine inviato
uv run python scripts/live/microtest.py --live # invia il round-trip REALE
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("microtest.py", USO, flag=("--live",), con_valore=())
main()
+84 -1
View File
@@ -10,20 +10,91 @@ monitor e la soglia di copertura 0.80 (la stessa del veto d'integrita' di DVOLSP
uv run python scripts/live/monitor_health.py # report
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
uv run python scripts/live/monitor_health.py --solo-orari # (#30) solo i sorveglianti a
# max_age<=6h (cuscino_watch, venue_watch, collect_chain), notifica solo il NUOVO
# rispetto a data/live/monitor_health_detto.json — pensato per girare ogni ora
# (cron_cuscino.sh), non per sostituire il giro giornaliero completo.
"""
from __future__ import annotations
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.monitor_health import MIN_COVERAGE, alerts_from, check_all # noqa: E402
from src.live.monitor_health import ( # noqa: E402
MIN_COVERAGE,
alert_nuovi,
alerts_from,
check_all,
marcatore_aggiornato,
monitors_orari,
)
from src.live.notifier import notify # noqa: E402
# (#30) marcatore "gia' detto" del giro orario: {"detti": [<chiave alert>], "ts": ...}. Vive fuori
# da src/ perche' e' I/O, non parte della misura pura.
DETTO = ROOT / "data" / "live" / "monitor_health_detto.json"
def _carica_detti(path: Path = DETTO) -> set[str]:
if not path.exists():
return set()
try:
return set(json.loads(path.read_text()).get("detti", []))
except Exception:
return set()
def _salva_detti(detti: set[str], path: Path = DETTO) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(
{"detti": sorted(detti), "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")},
indent=2))
def main_orari() -> int:
"""(#30) Giro orario: solo i sorveglianti a max_age<=6h, notifica solo il NUOVO rispetto al
marcatore. Exit 2 se c'e' un alert (anche gia' detto), 0 altrimenti — il giro giornaliero
resta l'unico posto dove "nessun alert nuovo" vuol dire davvero "tutto OK"."""
quiet = "--quiet" in sys.argv
rows = check_all(ROOT, monitors=monitors_orari())
alerts = alerts_from(rows)
detti = _carica_detti(DETTO) # DETTO letto qui, non nel default del parametro (monkeypatch)
nuovi = alert_nuovi(rows, detti)
aggiornato = marcatore_aggiornato(rows, detti)
if not quiet or alerts:
print("=" * 92)
print(" MONITOR HEALTH — giro orario (sorveglianti con max_age <= 6h)")
print("=" * 92)
for r in rows:
cov = f"{r['coverage']:.0%}" if r["coverage"] is not None else "-"
age = f"{r['age_h']:.0f}h" if r["age_h"] is not None else "-"
print(f" {r['name']:<20}{r['status']:>10}{age:>13}{cov:>10}")
inviato = True
if nuovi:
msgs = alerts_from(nuovi)
inviato = notify("🩺 MONITOR HEALTH — un sorvegliante orario non sta registrando",
{f"alert {i+1}": m for i, m in enumerate(msgs)})
for m in msgs:
print(f" ALERT (nuovo): {m}")
if not inviato:
print(" ⚠️ invio FALLITO — marcatore NON aggiornato: il prossimo giro ritenta")
if not nuovi or inviato:
_salva_detti(aggiornato, DETTO)
return 2 if alerts else 0
def main() -> int:
if "--solo-orari" in sys.argv:
return main_orari()
rows = check_all(ROOT)
alerts = alerts_from(rows)
@@ -55,5 +126,17 @@ def main() -> int:
return 0
USO = """uso: monitor_health.py [--quiet] [--solo-orari]
monitor_health.py — i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla.
uv run python scripts/live/monitor_health.py # report
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
uv run python scripts/live/monitor_health.py --solo-orari # (#30) solo i sorveglianti a
max_age<=6h, notifica solo il NUOVO rispetto al marcatore
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("monitor_health.py", USO, flag=("--quiet", "--solo-orari"), con_valore=())
raise SystemExit(main())
+8
View File
@@ -118,5 +118,13 @@ def main():
print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}")
USO = """uso: paper_combo.py [--reset] [--status]
uv run python scripts/live/paper_combo.py [--status|--reset]
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("paper_combo.py", USO, flag=("--status", "--reset"), con_valore=())
main()
+10
View File
@@ -87,5 +87,15 @@ def main():
print(f" posizioni correnti: {pf.current_positions()}")
USO = """uso: paper_portfolio.py [--reset] [--status]
uv run python scripts/live/paper_portfolio.py # avanza (init al 1o run)
uv run python scripts/live/paper_portfolio.py --status # solo stato
uv run python scripts/live/paper_portfolio.py --reset # azzera (riparte da ora)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("paper_portfolio.py", USO, flag=("--status", "--reset"), con_valore=())
main()
+163 -1
View File
@@ -53,6 +53,21 @@ MODELED_CAPITAL = 2000.0 # nominale, ribilanciamento continuo
REAL_CAPITAL = 600.0 # capitale mainnet reale
MIN_ORDER = 5.0 # min order Deribit -> sotto, il conto vero NON ribilancia
# --- GATE PRE-REGISTRATO `GATE PREVDAY-01` (2026-08-23, RESULTS-0822 §54) -----------------------
# Cablato qui il 2026-09-10 (issue #4): esisteva solo nel testo, e la revisione del 09/09 lo ha
# letto come "gate senza data". Decisione dell'operatore: tenere la data, cablare kill e veto.
GATE_DATE = "2027-06-21" # decisione piena: 7 condizioni (a)-(g), tutte necessarie
KILL_SHARPE = -0.50 # kill: Sharpe forward < -0,50 ...
KILL_MIN_ACTIVE_DAYS = 180 # ... su >= 180 giorni di barre ATTIVE (posizione != 0)
MIN_RECON_FRAC = 0.80 # veto d'integrita': >= 80% di barre ricostruibili dal feed
RECON_TOL = 1.5e-6 # net_modeled e' registrato a 6 decimali: mezzo ulp di tolleranza
RECENTI_GIORNI = 30 # "i minuti non registrati non devono crescere": ultimi 30g vs tutto
MIN_DIV_CRESCITA = 5 # ... ma UNA barra divergente su 1943 non e' una crescita (P14): la
# crescita richiede >= 5 divergenti recenti E tasso doppio del totale
# LENTE: il kill si misura sullo Sharpe GIORNALIERO (somma delle barre orarie per giorno UTC),
# la lente con cui e' misurato ogni altro sleeve del progetto — §54: sulla lente ORARIA lo stesso
# forward vale +2,04 contro +1,56 giornaliero. Al kill la famiglia NON si ri-ottimizza.
def build_bars() -> dict[str, pd.DataFrame]:
return {a: load(a, "1h").reset_index(drop=True) for a in ASSETS}
@@ -145,6 +160,151 @@ def advance(st: dict, dfs: dict) -> dict:
return st
def _returns_df() -> pd.DataFrame | None:
if not RETURNS_FILE.exists():
return None
r = pd.read_json(RETURNS_FILE, lines=True)
if r.empty:
return None
r["dt"] = pd.to_datetime(r["ts"], unit="ms", utc=True)
return r
def sharpe_giornaliero(r: pd.DataFrame, col: str = "net_modeled") -> tuple[float, int]:
"""PURA. Sharpe annualizzato sui rendimenti GIORNALIERI (somma delle barre orarie per giorno
UTC) -> (sharpe, n_giorni). nan sotto 30 giorni o a varianza nulla."""
d = r.groupby(r["dt"].dt.floor("D"))[col].sum()
if len(d) < 30 or d.std() == 0:
return float("nan"), int(len(d))
return float(d.mean() / d.std() * np.sqrt(365.25)), int(len(d))
def giorni_attivi(r: pd.DataFrame) -> int:
"""PURA. Giorni UTC con almeno una barra a posizione != 0 (il kill conta le barre ATTIVE).
⚠️ le posizioni registrate sono quelle del libro REAL-$600 (advance scrive `pr`), lo Sharpe
del kill e' MODELED: divergono solo se un ingresso da 0 vale < $5 di nozionale, cosa che a
vol-target 20% non succede (10/09: 81/81 giorni). Dichiarato, non riparato (D5)."""
att = r[(r["pos_btc"] != 0) | (r["pos_eth"] != 0)]
return int(att["dt"].dt.floor("D").nunique())
def kill_check(sharpe: float, n_attivi: int, today: pd.Timestamp) -> tuple[str, str]:
"""PURA. -> (stato, motivo). KILL solo con >= 180 giorni attivi E Sharpe < -0,50 (§54):
a quell'orizzonte P(uccidere un edge vivo a Sharpe 1,2) = 9,7%."""
if n_attivi < KILL_MIN_ACTIVE_DAYS:
return "NON_MATURO", (f"{n_attivi} giorni attivi < {KILL_MIN_ACTIVE_DAYS}: il kill non e' "
f"leggibile (mancano {KILL_MIN_ACTIVE_DAYS - n_attivi} giorni)")
if not np.isfinite(sharpe):
return "NON_MISURABILE", "Sharpe forward non misurabile"
if sharpe < KILL_SHARPE:
return "KILL", f"Sharpe {sharpe:+.2f} < {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi -> RITIRARE, senza ri-ottimizzare"
return "SUPERATO", f"Sharpe {sharpe:+.2f} >= {KILL_SHARPE:+.2f} su {n_attivi} giorni attivi"
def ricostruzione(r: pd.DataFrame, dfs: dict, start_ts: int) -> dict:
"""Rigioca la strategia CONGELATA sul feed di OGGI dallo `start_ts` del monitor e confronta
barra per barra `net_modeled` registrato con quello ricalcolato (ribilanciamento continuo,
la stessa aritmetica di `advance`). Una barra e' RICOSTRUIBILE se coincide entro RECON_TOL.
Cosa misura: quante barre registrate sono riproducibili dal dato certificato. Le divergenti
sono barre calcolate su un feed che il rebuild ha poi rivisto (§54: 67 su 1512, tutte
all'ora del cron) — e' la misura del veto d'integrita' (>= 80%, e i minuti persi non devono
crescere: qui, divergenti/giorno negli ultimi 30g contro l'intera finestra)."""
data = {}
for a in ASSETS:
df = dfs[a]
c = df["close"].values.astype(float)
rr = np.zeros(len(c)); rr[1:] = c[1:] / c[:-1] - 1.0
data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=rr, tgt=prevday_target(df))
idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS}
# posizione iniziale come init_state: target all'ultima barra <= start_ts
pos = {}
for a in ASSETS:
i0 = max(i for t, i in idx[a].items() if t <= start_ts)
pos[a] = float(data[a]["tgt"][i0])
ricalc, presenti = [], []
for t in r["ts"].astype("int64"):
t = int(t)
if any(t not in idx[a] for a in ASSETS):
ricalc.append(np.nan); presenti.append(False); continue
net = 0.0
for a in ASSETS:
i = idx[a][t]
tgt = float(data[a]["tgt"][i])
net += WEIGHT * (pos[a] * float(data[a]["r"][i]) - FEE_SIDE * abs(tgt - pos[a]))
pos[a] = tgt
ricalc.append(net); presenti.append(True)
ricalc = np.asarray(ricalc, dtype=float)
reg = r["net_modeled"].values.astype(float)
ok = np.isfinite(ricalc) & (np.abs(ricalc - reg) <= RECON_TOL)
n = len(reg)
giorni = r["dt"].dt.floor("D")
span_g = max(1, int(giorni.nunique()))
recenti = r["dt"] >= (r["dt"].max() - pd.Timedelta(days=RECENTI_GIORNI))
div_rec = int((~ok[recenti.values]).sum())
g_rec = max(1, int(giorni[recenti].nunique()))
return dict(n=n, ricostruibili=int(ok.sum()), frac=float(ok.sum() / n) if n else float("nan"),
assenti_nel_feed=int((~np.asarray(presenti)).sum()),
div_per_giorno=float((n - ok.sum()) / span_g),
div_recenti=div_rec, div_per_giorno_recenti=float(div_rec / g_rec),
divergenti_ts=[int(t) for t in r["ts"].values[~ok]][:20])
def veto_check(frac: float, div_g_tot: float, div_g_rec: float, div_rec: int) -> tuple[bool, str]:
"""PURA. Veto d'integrita' (blocca, non decide): sotto l'80% di barre ricostruibili, o con le
divergenze che CRESCONO negli ultimi 30 giorni, la finestra si ESTENDE invece di decidere."""
motivi = []
if not np.isfinite(frac) or frac < MIN_RECON_FRAC:
motivi.append(f"barre ricostruibili {frac:.1%} < {MIN_RECON_FRAC:.0%}")
if div_rec >= MIN_DIV_CRESCITA and div_g_rec > 2.0 * div_g_tot + 1e-12:
motivi.append(f"divergenze in crescita: {div_rec} negli ultimi {RECENTI_GIORNI}g "
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g sull'intera finestra)")
return (not motivi), ("; ".join(motivi) if motivi else
f"ricostruibili {frac:.1%}, divergenti {div_rec} negli ultimi {RECENTI_GIORNI}g "
f"({div_g_rec:.2f}/g contro {div_g_tot:.2f}/g totale)")
def print_gate(st: dict, dfs: dict, today: pd.Timestamp | None = None) -> dict:
"""Stampa lo stato del gate pre-registrato e lo RITORNA (per i test e per chi legge il log).
Non decide niente: al kill e alla data stampa cosa la regola dice, in maiuscolo."""
r = _returns_df()
out = dict(gate_date=GATE_DATE, kill="NON_MISURABILE", veto_ok=None)
print(f"\n GATE PRE-REGISTRATO `PREVDAY-01` (23/08, RESULTS-0822 §54) — decisione {GATE_DATE}")
if r is None or len(r) < 2:
print(" serie forward assente o troppo corta: niente da misurare")
return out
today = today or pd.Timestamp.now(tz="UTC").normalize()
sh, n_g = sharpe_giornaliero(r)
n_att = giorni_attivi(r)
stato, motivo = kill_check(sh, n_att, today)
out.update(sharpe_giornaliero=sh, n_giorni=n_g, n_attivi=n_att, kill=stato, kill_motivo=motivo)
print(f" Sharpe forward GIORNALIERO (MODELED): {sh:+.2f} su {n_g} giorni, {n_att} attivi"
f" [lente oraria {_sharpe_orario(r):+.2f}: non si cita, §54]")
if stato == "KILL":
print(f" *** KILL: {motivo} ***")
else:
print(f" kill (Sharpe < {KILL_SHARPE:+.2f} su >= {KILL_MIN_ACTIVE_DAYS}g attivi): {stato} — {motivo}")
rc = ricostruzione(r, dfs, int(st["start_ts"]))
ok, vm = veto_check(rc["frac"], rc["div_per_giorno"], rc["div_per_giorno_recenti"], rc["div_recenti"])
out.update(ricostruzione=rc, veto_ok=ok, veto_motivo=vm)
print(f" veto d'integrita': {'ok' if ok else '*** VETO — la finestra si ESTENDE ***'} — {vm}"
f" ({rc['ricostruibili']}/{rc['n']} barre, {rc['assenti_nel_feed']} assenti nel feed)")
if today >= pd.Timestamp(GATE_DATE, tz="UTC"):
print(f" *** DECISIONE DOVUTA ({GATE_DATE}): (a) DSR >= 0,95 sulla famiglia dichiarata "
f"(b) cella al buio == congelata (c) delta libro > +0,05 col funding (d) weights_tilt_null "
f"(e) ADDS+robust_oos+non-hedge sulla cella al buio (f) day_boundary_robust "
f"(g) Sharpe forward > 0 [{sh:+.2f}] — TUTTE necessarie ***")
else:
print(f" decisione fra {(pd.Timestamp(GATE_DATE, tz='UTC') - today).days} giorni; "
f"le condizioni (a)-(b) sono STRUTTURALI e il forward non le cambia (§54)")
return out
def _sharpe_orario(r: pd.DataFrame) -> float:
x = r["net_modeled"].astype(float)
return float(x.mean() / x.std() * np.sqrt(24 * 365.25)) if x.std() > 0 else float("nan")
def print_status(st: dict, dfs: dict):
days = (max(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) - st["start_ts"]) / 86400_000
rm = st["cap_modeled"] / MODELED_CAPITAL - 1
@@ -156,7 +316,9 @@ def print_status(st: dict, dfs: dict):
print(f" MODELED ($2000 nominale): {rm*100:+6.2f}% eq ${st['cap_modeled']:.2f} maxDD {st['dd_modeled']*100:.1f}%")
print(f" REAL-$600 (min-order $5) : {rr*100:+6.2f}% eq ${st['cap_real']:.2f} maxDD {st['dd_real']*100:.1f}%")
print(f" -> fill-haircut MODELED-REAL: {(rm-rr)*100:+.2f} pp (lo scettico l'aveva segnalato)")
print(f" log: {RETURNS_FILE}\n")
print(f" log: {RETURNS_FILE}")
print_gate(st, dfs)
print()
def main():
+10
View File
@@ -224,5 +224,15 @@ def main() -> None:
print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO)
USO = """uso: paper_regen.py [--solo V]
paper_regen.py — RIGENERA le serie dei forward-monitor rotti da `advance()` (§5.1).
uv run python scripts/live/paper_regen.py # tutti e 4
uv run python scripts/live/paper_regen.py --solo paper_statarb
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("paper_regen.py", USO, flag=(), con_valore=("--solo",))
main()
+48
View File
@@ -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)
+16 -1
View File
@@ -13,11 +13,15 @@ from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify # noqa: E402
from src.live.scale_watch import ALLARME, OK, run_once # noqa: E402
RUNS = ROOT / "data" / "scale_watch" / "runs.jsonl"
def _sender(testo: str):
try:
@@ -31,6 +35,7 @@ def main() -> None:
secco = "--secco" in sys.argv
quiet = "--quiet" in sys.argv
rep = run_once(sender=None if secco else _sender)
registra_giro(RUNS, stato=rep["stato"])
icona = {OK: "✅", ALLARME: "🚨"}.get(rep["stato"], "⚠️")
if quiet:
fb = rep["fallback"]
@@ -58,5 +63,15 @@ def main() -> None:
print(f"\n invio: {rep['invio']}")
USO = """uso: scale_watch.py [--quiet] [--secco]
uv run python scripts/live/scale_watch.py # osserva e stampa, invia se ALLARME
uv run python scripts/live/scale_watch.py --secco # osserva e stampa, NON invia nulla
uv run python scripts/live/scale_watch.py --quiet # una riga sola (per il cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("scale_watch.py", USO, flag=("--quiet", "--secco"), con_valore=())
main()
+97 -17
View File
@@ -25,17 +25,63 @@ import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.monitor_health import MONITORS
from src.live.notifier import send
HORIZONS = (30, 90, 180)
GATES = [("STATARB-RESID", date(2026, 9, 27), "data/paper_statarb/returns.jsonl", "net_modeled"),
("XSR01", date(2026, 10, 23), "data/paper_xsr/returns.jsonl", "net_modeled")]
def _gate_date(nome_monitor: str, cablata: date) -> date:
"""(#35 C14) la data del gate viene da monitor_health.MONITORS (`MonitorSpec.gate`), la
STESSA tabella che "METTE A RISCHIO il gate" negli alert dei sorveglianti: una fonte, non
una seconda dichiarazione duplicata di CLAUDE.md §4. Il campo e' prosa libera
("YYYY-MM-DD — descrizione"): si legge la data solo se e' nella forma attesa. `cablata' e'
rete di sicurezza se il monitor sparisse o il campo cambiasse forma — non deve succedere per
i due gate qui sotto, oggi allineati (verificato a mano contro CLAUDE.md §4)."""
for m in MONITORS:
if m.name == nome_monitor and m.gate:
try:
return date.fromisoformat(m.gate[:10])
except ValueError:
break
return cablata
GATES = [("STATARB-RESID", _gate_date("paper_statarb", date(2026, 9, 27)),
"data/paper_statarb/returns.jsonl", "net_modeled"),
("XSR01", _gate_date("paper_xsr", date(2026, 10, 23)),
"data/paper_xsr/returns.jsonl", "net_modeled")]
def _fmt_usd(x: float) -> str:
return f"${x:,.2f}"
def _soglia_accensione(segni: list[int], neg_refs: list[float]) -> float | None:
"""(#35 C3) Quale riferimento, superato, accende TP01 (mean(segni) > 0)?
I segni sono sempre in {-1,+1}, mai 0 (trend_state li costruisce con `1 if px>refs[h]
else -1`: nessun pareggio possibile). Con k segni negativi da far diventare positivi, la
somma cresce di 2 per flip: serve il piu' piccolo k tale che sum(segni)+2k > 0. La soglia
e' il k-esimo riferimento negativo piu' basso (k=1 -> il minimo, come prima del fix; k=2 ->
il secondo, ecc.) — non sempre il minimo: con TUTTI i riferimenti negativi, superare solo il
piu' basso lascia la media <= 0 e TP01 a 0 (bug segnalato in #35 C3).
None se la media e' gia' positiva (nessuna soglia da dare) o se k supera il numero di
riferimenti negativi disponibili (non dovrebbe accadere: con n segni, flippare tutti gli n
negativi porta comunque la somma a +n > 0, quindi k <= len(neg_refs) sempre — resta una
guardia difensiva, non un caso noto)."""
s = sum(segni)
if s > 0 or not neg_refs:
return None
k = 0
while s + 2 * k <= 0:
k += 1
if k > len(neg_refs):
return None
return sorted(neg_refs)[k - 1]
def trend_state() -> list[str]:
"""Stato TSMOM per asset + quanto manca all'accensione (2 orizzonti su 3 positivi).
@@ -59,32 +105,51 @@ def trend_state() -> list[str]:
tgt = max(0.0, m)
line = f" {a} {px:,.0f} · segni " + "/".join(f"{sg[h]:+d}" for h in HORIZONS) + \
f" → TP01 {tgt:.2f}"
neg = [(h, refs[h]) for h in HORIZONS if sg[h] < 0]
if neg and tgt == 0.0:
h, r = min(neg, key=lambda t: t[1])
line += f"\n accensione a {r:,.0f} ({(r/px-1)*100:+.1f}%)"
if tgt == 0.0:
r = _soglia_accensione(list(sg.values()), [refs[h] for h in HORIZONS if sg[h] < 0])
if r is not None:
line += f"\n accensione a {r:,.0f} ({(r/px-1)*100:+.1f}%)"
out.append(line)
except Exception as e:
out.append(f" {a}: stato trend non calcolabile ({type(e).__name__})")
return out
def book_state() -> list[str]:
"""Conto reale, posizioni e target netto correnti. Sola lettura."""
def book_state() -> tuple[list[str], bool | None]:
"""Conto reale, posizioni e target netto correnti. Sola lettura.
(#35 C14) Ritorna anche se il libro e' DAVVERO flat (nessuna posizione, nessun target: stessa
condizione che questa funzione gia' usa riga per riga per stampare "flat" invece di "pos ->
target"): serve a `build()` per decidere se l'header "Perche' non opera" e' vero o
fuorviante, SENZA una lettura nuova — None se lo stato del libro non e' leggibile (in quel
caso non sappiamo dire se opera)."""
try:
from src.live.book import book_report
r = book_report()
eq = r.get("equity")
lines = [f" equity {_fmt_usd(eq)}" if eq else " equity non leggibile"]
flat = True
for a in r.get("assets", []):
pos, net = a.get("position_usd", 0.0), a.get("net_target", 0.0)
stato = "flat" if abs(pos) < 1 and abs(net) < 1 else f"pos {_fmt_usd(pos)} → target {_fmt_usd(net)}"
is_flat = abs(pos) < 1 and abs(net) < 1
flat = flat and is_flat
stato = "flat" if is_flat else f"pos {_fmt_usd(pos)} → target {_fmt_usd(net)}"
lines.append(f" {a['asset']}: {stato} (TP {a.get('tp_frac', 0):+.2f} · SKH {a.get('skh_sign', 0):+d})")
if r.get("skh_error"):
lines.append(f" ⚠️ SKH feed KO: {r['skh_error']}")
return lines
return lines, flat
except Exception as e:
return [f" ⚠️ stato libro non leggibile: {type(e).__name__}: {e}"]
return [f" ⚠️ stato libro non leggibile: {type(e).__name__}: {e}"], None
def _fmt_giorni(giorni: int) -> str:
"""(#35 C14) tre casi distinti, non un segno "−" cablato davanti a un intero che puo' essere
negativo: con giorni=-1 quel cablaggio stampava "(−-1g)", un doppio segno illeggibile."""
if giorni > 0:
return f"-{giorni}g"
if giorni == 0:
return "oggi"
return f"scaduto da {-giorni}g"
def monitors() -> list[str]:
@@ -92,20 +157,21 @@ def monitors() -> list[str]:
for name, dec, path, key in GATES:
p = ROOT / path
giorni = (dec - date.today()).days
gate_txt = f"gate {dec} ({_fmt_giorni(giorni)})"
if not p.exists():
out.append(f" {name}: in attesa della prima barra · gate {dec} (−{giorni}g)")
out.append(f" {name}: in attesa della prima barra · {gate_txt}")
continue
try:
rows = [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
if not rows:
out.append(f" {name}: 0 barre · gate {dec} (−{giorni}g)")
out.append(f" {name}: 0 barre · {gate_txt}")
continue
r = np.array([x[key] for x in rows], float)
sh = float(r.mean() / r.std() * np.sqrt(365)) if r.std() > 0 else 0.0
tot = float(np.prod(1 + r) - 1) * 100
out.append(f" {name}: {len(rows)}g · {tot:+.2f}% · Sh {sh:+.2f} · gate {dec} (−{giorni}g)")
out.append(f" {name}: {len(rows)}g · {tot:+.2f}% · Sh {sh:+.2f} · {gate_txt}")
except Exception:
out.append(f" {name}: log illeggibile · gate {dec} (−{giorni}g)")
out.append(f" {name}: log illeggibile · {gate_txt}")
return out
@@ -129,8 +195,13 @@ def last_trade() -> str:
def build() -> str:
oggi = datetime.now(timezone.utc).strftime("%d/%m %H:%M UTC")
L = [f"📊 <b>Book Deribit — {oggi}</b>", "", "<b>Conto</b>"]
L += book_state()
L += ["", "<b>Perche' non opera</b>"]
righe_conto, flat = book_state()
L += righe_conto
# (#35 C14) "Perche' non opera" e' vero solo se il libro e' DAVVERO flat (nessuna posizione,
# nessun target: gia' letto sopra da book_state, nessuna lettura nuova). Se non e' flat (ha
# posizioni o un target diverso da zero) o non si sa (book non leggibile), l'header fisso
# mentirebbe: si usa un titolo neutro.
L += ["", "<b>Perche' non opera</b>" if flat else "<b>Stato trend</b>"]
L += trend_state()
L += ["", "<b>Ordini</b>", last_trade()]
L += ["", "<b>Monitor in osservazione</b>"]
@@ -146,5 +217,14 @@ def main() -> None:
print("inviato" if send(txt) else "NON inviato (config Telegram assente o rete KO)")
USO = """uso: telegram_daily.py [--dry-run]
uv run python scripts/live/telegram_daily.py # calcola e invia
uv run python scripts/live/telegram_daily.py --dry-run # stampa e basta, non invia
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("telegram_daily.py", USO, flag=("--dry-run",), con_valore=())
main()
+67 -7
View File
@@ -17,6 +17,7 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live import tradesdb as T # noqa: E402
from src.live.notifier import notify # noqa: E402
def sync(verbose: bool = True) -> dict:
@@ -25,16 +26,51 @@ def sync(verbose: bool = True) -> dict:
fills = [f for r in runs for f in r.fills]
n_f = T.upsert_fills(con, fills)
n_e = T.upsert_equity(con, [(r.ts_utc, r.equity, "cron_book") for r in runs if r.equity is not None])
# #40 (TradingBook#11): arricchimento PRIMA dei round-trip, cosi' il FIFO vede gia' i prezzi
# pieni e le ore vere delle righe abbinate al jsonl, non quelle (arrotondate, di inizio giro)
# di cron_book.log. `righe_jsonl` si legge una volta sola e serve anche al controllo di
# conteggio (#37 C11) qui sotto.
righe_jsonl = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else []
n_arricchiti, n_ambigui = T.arricchisci_da_jsonl(con, righe_jsonl)
n_r = T.rebuild_roundtrips(con)
if runs:
T.set_meta(con, "ultimo_giro", runs[-1].ts_utc)
T.set_meta(con, "ultimo_sync", T.ora())
tot = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"]
tot_e = con.execute("SELECT COUNT(*) c FROM equity").fetchone()["c"]
# issue #37 (C11). Dal 2026-08-24T01:07Z (T.CUTOFF_TS_VERO) book_executions.jsonl porta l'ora
# VERA per-fill (docstring di tradesdb.py). Un conteggio diverso in quella finestra segnala
# che una delle due fonti sta perdendo fill — questo controllo resta indipendente
# dall'arricchimento sopra (che abbina riga per riga, non conta): un conteggio uguale non
# esclude un abbinamento ambiguo, e viceversa. Notify UNA volta per episodio: marcatore in
# `meta` (stessa regola di fee_watch #37 B9 — scritto solo se l'invio riesce, azzerato
# quando i conteggi tornano uguali).
n_jsonl_dal = sum(1 for r in righe_jsonl if str(r.get("ts_utc", "")) >= T.CUTOFF_TS_VERO)
n_db_dal = con.execute("SELECT COUNT(*) c FROM fills WHERE ts_utc >= ?",
(T.CUTOFF_TS_VERO,)).fetchone()["c"]
diverge = n_jsonl_dal != n_db_dal
gia_detto = T.get_meta(con, "conteggio_diverso_detto") == "1"
if diverge:
msg = f"⚠️ trades.db: {n_jsonl_dal} fill nel jsonl, {n_db_dal} nel DB dal 24/08"
if verbose:
print(f" {msg}")
if not gia_detto:
if notify("⚠️ trades.db — conteggio fill diverso dal jsonl dal 24/08",
{"fill nel jsonl": n_jsonl_dal, "fill nel DB": n_db_dal}):
T.set_meta(con, "conteggio_diverso_detto", "1")
elif gia_detto:
T.set_meta(con, "conteggio_diverso_detto", "0")
if verbose:
print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}")
print(f" arricchiti dal jsonl: {n_arricchiti} | ambigui: {n_ambigui}")
con.close()
return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot)
return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot,
jsonl_dal_24_08=n_jsonl_dal, db_dal_24_08=n_db_dal,
arricchiti=n_arricchiti, ambigui=n_ambigui)
def reconcile() -> None:
@@ -42,8 +78,8 @@ def reconcile() -> None:
fills = [f for r in runs for f in r.fills]
righe = T.parse_executions_jsonl(T.EXEC_JSONL.read_text()) if T.EXEC_JSONL.exists() else []
rec = T.reconcile(fills, righe)
print(f"\n cron_book.log : {len(fills)} fill (con ORA VERA)")
print(f" book_executions : {len(righe)} righe (senza ora)")
print(f"\n cron_book.log : {len(fills)} fill (ora di inizio giro)")
print(f" book_executions : {len(righe)} righe (ora VERA per-fill dal 24/08, #37 C11)")
print(f" concordano : {len(rec['ok'])}")
print(f" solo nel log : {len(rec['solo_log'])}")
print(f" solo nel jsonl : {len(rec['solo_jsonl'])}")
@@ -91,8 +127,22 @@ def report() -> None:
print(f" movimenti capitale : {r['certi']:+,.2f} certi | {r['ambigui']:+,.2f} ambiguo/i"
f" (restano nel P&L, dichiarati)")
for ev in r["eventi"]:
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}]")
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti certi)")
mk = ev.get("mercato_max_pct")
if ev.get("dichiarazione"):
dettaglio = ev["dichiarazione"]
elif mk is None:
dettaglio = ev.get("mercato_nota") or "mercato non misurabile"
else:
dettaglio = f"mercato max {100 * mk:.2f}% a leva piena, salto {100 * ev['pct']:+.1f}%"
print(f" {ev['ts_dopo'][:16]} {ev['delta']:+,.2f} [{ev['classe']}: {dettaglio}]")
for av in r.get("avvisi") or []:
print(f" ⚠️ {av}")
if r["trading"] is None:
print(f" trading da arming : n/d ({r['motivo']})")
else:
print(f" trading da arming : {r['trading']:+,.2f} (equity al netto dei movimenti "
f"certi; l'ora di un movimento RILEVATO esce intera, quella di un DICHIARATO "
f"resta — vedi journal.rendimento_twr)")
if r["twr"] is None:
print(f" TWR : n/d ({r['motivo']})")
else:
@@ -111,15 +161,25 @@ def report() -> None:
if ap:
print(" posizioni aperte :")
for a, d in sorted(ap.items()):
print(f" {a} {d['qty']:.4f} @ medio ${d['prezzo_medio']:,.2f} (dal {d['dal'][:16]})")
marca = "" if d.get("direzione", "long") == "long" else " SHORT" # #32 C2
print(f" {a}{marca} {d['qty']:.4f} @ medio ${d['prezzo_medio']:,.2f} "
f"(dal {d['dal'][:16]})")
else:
print(" posizioni aperte : nessuna (flat)")
print(f" ultimo sync : {T.get_meta(con, 'ultimo_sync', 'mai')}")
con.close()
USO = """uso: trades_db.py [--sync] [--report] [--reconcile] [--quiet]
--sync (default) legge log e jsonl e aggiorna data/live/trades.db — idempotente
--report stato del libro di bordo + P&L (TWR, non e1/e0)
--reconcile incrocio delle tre fonti sui fill (log x jsonl x venue)
--quiet con --sync: non stampa il riepilogo"""
if __name__ == "__main__":
args = sys.argv[1:]
from src.live.cli import valida
args = valida("trades_db.py", USO, flag=("--sync", "--report", "--reconcile", "--quiet"))
if "--reconcile" in args:
reconcile()
elif "--report" in args:
+89 -12
View File
@@ -59,8 +59,20 @@ la size. Percio' si SPAZIANO gli ordini (`--pausa`) e si fa backoff sul TEMPO, r
solo dopo rifiuti ripetuti. Dimezzare la size a ogni rifiuto, come faceva la prima versione, e' la
reazione sbagliata al sintomo giusto: consuma i tentativi senza toccare la causa.
uv run python scripts/live/usde_convert.py --quota 0.70 # piano, nessun ordine
uv run python scripts/live/usde_convert.py --quota 0.70 --esegui # manda l'ordine
uv run python scripts/live/usde_convert.py --quota 0.64 # piano, nessun ordine (0,64 = bersaglio di cuscino_watch)
uv run python scripts/live/usde_convert.py --quota 0.64 --esegui # manda l'ordine
LOCK SUL CONTO (#36). Un lancio a MANO con --esegui condivide il conto con cuscino_watch e
book_execute (cron_cuscino.sh/cron_book.sh, entrambi dentro `flock -n logs/.conto.lock`): senza
lo stesso lock un ordine a mano nello stesso minuto del cron puo' convertire due volte verso
bersagli diversi (limite D5 gia' dichiarato sopra). Si lancia quindi con:
flock logs/.conto.lock uv run python scripts/live/usde_convert.py --quota 0.64 --esegui
⚠️ Qui NON senza `-n`: un lancio a mano puo' aspettare il turno invece di saltare. Questo script
stesso NON prende il lock (vedi cuscino_watch.py): se lo facesse e girasse come figlio di
cuscino_watch (che il lock lo tiene gia'), un secondo `flock -n` lo troverebbe occupato e
fallirebbe subito, per costruzione, ogni singola volta.
"""
from __future__ import annotations
@@ -88,15 +100,63 @@ EPS_CUSCINO = 0.01 # 1 centesimo: il criterio e' ">= cuscino", e
# questo epsilon il rumore float boccia il caso di pareggio.
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
"""Cuscino USDC di REGOLAMENTO = disaster-SL sulla massima esposizione lorda del libro.
Derivato da config/live.json + src/live/book.py, mai ridichiarato qui."""
cfg = json.loads((ROOT / "config" / "live.json").read_text())
sl = float(cfg["disaster_sl_pct"])
from src.live import book as B
frac, n_asset = float(B.WEIGHT), 2
lordo_max = equity_tot * frac * n_asset
return lordo_max * sl, f"{n_asset} asset x frac {frac} x SL {sl:.0%} su ${equity_tot:,.2f}"
# Il cuscino di regolamento vive in src/live/usde.py dal 10/09 (issue #3): cuscino_watch lo
# sorveglia ogni ora e questo script lo rispetta a ogni piano — due lettori, UNA formula (P1).
# L'alias resta per r0906_usde_tetto_sonda, che lo importa da qui.
cuscino_richiesto_usd = U.cuscino_richiesto_usd
# issue #26: label dei NOSTRI ordini sullo spot, per riconoscere i residui che questo script
# stesso ha lasciato aperti (mai altri: un ordine con label diversa non e' nostro e non si tocca).
LABEL_PREFIX = "usde-quota-"
def _cancella(c, order_id: str) -> tuple[bool, str]:
"""Cancella UN ordine. -> (ok, motivo se no). NON solleva: il chiamante decide (P3) — la
lettura degli ordini aperti solleva (P5), la cancellazione di un singolo ordine no, perche'
il chiamante deve poter fermarsi con un messaggio invece di un traceback."""
try:
resp = c._unwrap(c._post("/mcp-deribit/tools/cancel_order", {"order_id": order_id})) or {}
except Exception as e: # noqa: BLE001 — si registra (P3)
return False, f"{type(e).__name__}: {e}"
if not isinstance(resp, dict) or resp.get("error") or resp.get("state") == "error":
return False, str(resp.get("error", resp)) if isinstance(resp, dict) else str(resp)
return True, ""
def cancella_residui(c) -> list[str]:
"""issue #26 — GTC mai cancellati. Il gateway cerbero-mcp NON accetta `time_in_force`
(PlaceOrderReq non ha il campo, extra ignorati: gitea:Adriano/Cerbero-mcp#1): un limit
riempito solo in parte resta APERTO per sempre e si accumula a ogni giro invece di sparire
con l'ordine. Qui si legge lo spot U.SPOT e si cancella SOLO cio' che porta la NOSTRA label
(`usde-quota-*`): un ordine con altra label non e' nostro e non si tocca.
-> lista degli order_id cancellati. Se la LETTURA degli ordini aperti fallisce SOLLEVA: non
si piazza un nuovo ordine senza sapere cosa c'e' gia' aperto (P5, non si ordina al buio)."""
# non e' verificato sotto quale valuta Deribit elenchi lo spot USDE_USDC (quotazione USDC o
# base USDE): si interrogano entrambe e si unisce per order_id, cosi' non si perde il residuo
# se il venue lo elenca sotto la base.
visti = {}
for valuta in ("USDC", "USDE"):
out = c._unwrap(c._post("/mcp-deribit/tools/get_open_orders",
{"currency": valuta, "kind": "spot", "type": "all"}))
ordini = out if isinstance(out, list) else (out.get("orders", []) if isinstance(out, dict) else [])
for o in ordini:
if isinstance(o, dict) and o.get("order_id") is not None:
visti[o["order_id"]] = o
ordini = list(visti.values())
cancellati = []
for o in ordini:
if not isinstance(o, dict):
continue
instrument = o.get("instrument") or o.get("instrument_name")
label = o.get("label") or ""
if instrument == U.SPOT and label.startswith(LABEL_PREFIX):
oid = o.get("order_id")
ok, motivo = _cancella(c, oid)
if not ok:
raise RuntimeError(f"cancellazione del residuo {oid} ({label}) fallita: {motivo}")
cancellati.append(oid)
return cancellati
def book_top(timeout: float = 5.0) -> tuple[float | None, float | None]:
@@ -165,7 +225,9 @@ def piano(eq_usdc: float, eq_usde: float, px: float, quota_target: float,
motivi.append(f"gia' a bersaglio ({quota_ora:.1%} vs {quota_target:.0%}, toll {TOLL:.1%})")
if abs(delta_usd) < MIN_ORDER_USD:
motivi.append(f"delta ${abs(delta_usd):,.2f} sotto il minimo ${MIN_ORDER_USD:,.2f}")
if cap_usd is not None and val_target > cap_usd + EPS_CUSCINO:
# il tetto del venue limita l'ACQUISTO di USDE: una VENDITA va sempre verso il tetto, e bloccarla
# renderebbe morta la riconversione automatica di cuscino_watch (revisione fable 10/09)
if side == "buy" and cap_usd is not None and val_target > cap_usd + EPS_CUSCINO:
motivi.append(
f"TETTO DEL VENUE: bersaglio ${val_target:,.2f} sopra ${cap_usd:,.2f} "
f"({cap_frac:.1%} dell'equity, misurato {cfg.get('venue_cap_misurato')}). "
@@ -235,6 +297,9 @@ def main() -> int:
return 0
LOG.parent.mkdir(parents=True, exist_ok=True)
residui = cancella_residui(c)
print(f" residui cancellati all'avvio: {len(residui)}"
+ (f" ({', '.join(residui)})" if residui else ""))
chunk, fatti, tot_filled, prezzi, falliti = a.chunk, [], 0.0, [], 0
for _ in range(60):
eq_usdc, eq_usde, px, _f = leggi_stato(c)
@@ -277,6 +342,18 @@ def main() -> int:
"amount": amt, "filled": filled})
print(f" · {pp['side']} {amt:,.0f} -> {order.get('order_state')} filled {filled:,.0f}"
f" (quota {pp['quota_ora']:.1%} -> bersaglio {a.quota:.0%})")
# issue #26: un GTC riempito solo in parte (o per niente) resta APERTO — il gateway non
# accetta time_in_force, quindi la cancellazione e' tutta qui. Se la cancel fallisce si
# FERMA il ciclo: mai un secondo ordine mentre il residuo del primo e' ancora vivo.
if filled < amt:
oid = order.get("order_id")
ok, motivo = _cancella(c, oid)
fatti[-1]["cancellato"] = True if ok else f"errore: {motivo}"
if not ok:
print(f" ✗ cancellazione del residuo {oid} FALLITA: {motivo}"
" — mi fermo (mai un secondo ordine con un residuo vivo)")
break
print(f" · residuo cancellato: order_id {oid} (filled {filled:,.0f}/{amt:,.0f})")
if filled <= 0:
print(" ✗ ordine accettato ma non fillato — mi fermo"); break
time.sleep(a.pausa)
+115 -61
View File
@@ -58,14 +58,27 @@ EPS_REWARD = 1e-3 # USDE: sotto e' rumore di lettura; il reward atteso a $500
FILL_TS = "2026-08-26T13:04:32Z" # conversione eseguita: 500 USDE @ 1.0003
VERDETTO_DOPO = "2026-08-29T12:30:00Z" # ultima finestra reward del 29/08 conclusa
# La QUOTA e' dell'operatore, e il 2026-08-29 e' stata RINVIATA a lunedi' 31/08 per decidere su
# piu' di una finestra invece che su una (il 27/08 aveva pagato ZERO, il 28/08 +0.052 USDE: fra
# le due letture ci sono 3,80% e 1,90% annuo, cioe' tutta la differenza che conta). N9: una
# decisione rinviata senza un promemoria e' rinviata per sempre — da lunedi' il watch lo dice
# ogni giorno finche' la riga non viene tolta.
DECISIONE_QUOTA_DAL = "2026-08-31" # lunedi': si decide la quota
EPS_GIORNI = 0.5 # sotto mezza giornata l'APR non si annualizza
# --- lettura dei trade spot: i DUE limiti del canale, misurati il 2026-09-10 -----------------
# (1) `count` massimo di Deribit per get_user_trades_by_instrument: 1000. Il 07/09 il lettore
# chiedeva limit=50 e la sonda del 06/09 aveva fatto 54 ordini: i 4 non letti (400 USDE)
# sono finiti nel «reward» (issue #2). Se il venue restituisce ESATTAMENTE `limit` righe la
# lista puo' essere troncata e la somma NON e' leggibile (P12: non si attribuisce).
# (2) il gateway chiama l'endpoint senza `historical`/timestamp: Deribit restituisce solo le
# ultime 24h — DOCUMENTATO ("Accessing historical trades and orders using API": recent
# trades 24h, orders 30 min, count max 1000) e misurato (il fill 08/09 06:47Z invisibile
# alle 12:55Z del 10/09, quello del 09/09 21:47Z visibile). Se l'ultima lettura e' piu'
# vecchia della finestra, i trade fra la lettura e il bordo sono INVISIBILI e la somma non
# e' leggibile.
# ⚠️ BUCO DICHIARATO (D5): il cron gira ogni 24h + pochi secondi; con la tolleranza di 5 min
# i trade nei primi ≤5 min dopo la lettura precedente sarebbero invisibili e sommati come
# «leggibile». Innocuo: nessun attrezzo converte USDE alle 12:35 UTC, e senza tolleranza il
# giro quotidiano sarebbe SEMPRE «non leggibile» e il tasso non avrebbe mai finestre nuove.
TRADE_LIMIT = 1000
FINESTRA_GATEWAY_MS = 24 * 3_600_000
TOLL_FINESTRA_MS = 5 * 60_000
def _parse(ts: str) -> datetime:
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
@@ -143,13 +156,6 @@ def rendimento(records: list[dict]) -> dict:
nota=f"{len(pagate)}/{len(usabili)} finestre hanno pagato")
def quota_da_decidere(now: datetime, verdetto_corrente: str) -> bool:
"""PURA. La decisione di quota e' dovuta? Solo se il conto e' IDONEO e il rinvio e' scaduto."""
if verdetto_corrente != "IDONEO":
return False
return now.date().isoformat() >= DECISIONE_QUOTA_DAL
def condizioni(rec: dict | None, c: dict) -> set[str]:
"""PURA. Condizioni di allerta attive su un record."""
if not rec:
@@ -169,6 +175,20 @@ def condizioni(rec: dict | None, c: dict) -> set[str]:
return att
def nuove_condizioni(att_now: set[str], att_prev: set[str], prev: dict | None) -> set[str]:
"""PURA (issue #29 B2). Le condizioni da notificare in questo giro.
Fino a qui lo stato si salvava PRIMA dell'invio e il ritorno di notify() non si leggeva: un
invio fallito restava "gia' detto" per l'episodio intero (l'ora dopo att_prev diceva ancora
che la condizione c'era, quindi non era piu' "nuova"). Schema di cuscino_watch.py: una
condizione conta come "gia' detta" solo se era attiva l'ultima volta E il suo invio e'
RIUSCITO — cioe' compare in `allertati` del record precedente. Un record vecchio senza
`allertati` (scritto prima di questo fix) vale come "tutto allertato": nessun rispam
retroattivo delle condizioni gia' viste."""
confermate_prev = set((prev or {}).get("allertati", att_prev))
return att_now - (att_prev & confermate_prev)
def _safe_client():
try:
from src.live.deribit import DeribitRead
@@ -177,14 +197,36 @@ def _safe_client():
return None
def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]:
"""Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms. None = non leggibile."""
def trade_copertura(n_righe: int, prev_ts_ms: int, now_ms: int,
limit: int = TRADE_LIMIT, finestra_ms: int = FINESTRA_GATEWAY_MS,
toll_ms: int = TOLL_FINESTRA_MS) -> str | None:
"""PURA. None se la lista di trade COPRE l'intervallo (prev, now]; altrimenti il motivo per cui
NON lo copre — e allora la somma non si usa (P12: un reward calcolato su una lista incompleta
e' un'invenzione, il 07/09 valeva 400 USDE)."""
if n_righe >= limit:
return f"lista troncata: {n_righe} righe = limite {limit} del venue"
if now_ms - prev_ts_ms > finestra_ms + toll_ms:
ore = (now_ms - prev_ts_ms) / 3_600_000
return (f"ultima lettura {ore:.1f}h fa, oltre la finestra di {finestra_ms / 3_600_000:.0f}h "
f"del gateway: i trade piu' vecchi sono invisibili")
return None
def _trades_spot_da(client, ts_ms: int, now_ms: int | None = None
) -> tuple[float | None, int | None, str | None]:
"""Somma con segno (buy +, sell -) dell'USDE scambiato spot dopo ts_ms.
-> (somma, n, motivo). somma None = non leggibile, e `motivo` dice perche' (P4)."""
if client is None:
return None, None
return None, None, "gateway non raggiungibile"
try:
rows = client.trade_history(U.SPOT, limit=50)
except Exception:
return None, None
rows = client.trade_history(U.SPOT, limit=TRADE_LIMIT)
except Exception as e:
return None, None, f"trade_history: {type(e).__name__}"
if now_ms is None:
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
motivo = trade_copertura(len(rows), ts_ms, now_ms)
if motivo is not None:
return None, None, motivo
tot, n = 0.0, 0
for t in rows:
if int(t.get("timestamp") or 0) <= ts_ms:
@@ -192,7 +234,7 @@ def _trades_spot_da(client, ts_ms: int) -> tuple[float | None, int | None]:
amt = float(t.get("amount") or 0)
tot += amt if (t.get("direction") or "").lower() == "buy" else -amt
n += 1
return round(tot, 8), n
return round(tot, 8), n, None
def main() -> int:
@@ -221,10 +263,11 @@ def main() -> int:
if eq_usde is not None:
usd, _nota = U.valuta(eq_usde, px)
trades_usde, n_trades = (None, None)
trades_usde, n_trades, trades_motivo = (None, None, None)
an = dict(delta=None, reward_stimato=None, reward_rilevato=False, con_trade=None)
if stato == "OK" and prev is not None and prev.get("eq_usde") is not None:
trades_usde, n_trades = _trades_spot_da(client, int(prev["ts"]))
trades_usde, n_trades, trades_motivo = _trades_spot_da(
client, int(prev["ts"]), int(now.timestamp() * 1000))
an = analizza(prev, eq_usde, trades_usde)
quota = None
@@ -237,59 +280,63 @@ def main() -> int:
stato=stato, motivo_blind=motivo_blind,
eq_usde=eq_usde, px=px, px_fonte=px_fonte, usd=usd,
eq_usdc=eq_usdc, quota=quota,
trades_usde=trades_usde, n_trades=n_trades, **an,
trades_usde=trades_usde, n_trades=n_trades, trades_motivo=trades_motivo, **an,
)
v, v_motivo = verdetto(records + [rec], now)
rec["verdetto"], rec["verdetto_motivo"] = v, v_motivo
STATE.parent.mkdir(parents=True, exist_ok=True)
with STATE.open("a") as fh:
fh.write(json.dumps(rec) + "\n")
# --- allarmi: transizioni (warn/quota/blind), ripetizione solo per il crit ---
# --- allarmi: transizioni (warn/quota/blind), ripetizione solo per il crit -----------------
# issue #29 (B2): fino a qui lo stato si salvava PRIMA dell'invio e il ritorno di notify()
# non si leggeva — un invio fallito restava "gia' detto" per l'episodio intero. Schema di
# cuscino_watch.py: `allertati` nel record = le condizioni il cui invio E' RIUSCITO. Lo stato
# si scrive una volta sola, a fine giro, con `allertati` gia' dentro (vedi sotto).
att_now, att_prev = condizioni(rec, c), condizioni(prev, c)
nuove = att_now - att_prev
nuove = nuove_condizioni(att_now, att_prev, prev)
confermate_prev = set((prev or {}).get("allertati", att_prev))
inviati = []
allertati = (att_now & att_prev & confermate_prev) - {"DEPEG_CRIT"} # attive, gia' confermate
if "DEPEG_CRIT" in att_now:
notify("🚨 USDE DEPEG", {"indice": px, "soglia": c["depeg_crit"],
"collaterale": f"${usd:,.0f}" if usd else "?",
"azione": "valutare riconversione — la quota e' il controllo (N4)"})
ok = notify("🚨 USDE DEPEG", {"indice": px, "soglia": c["depeg_crit"],
"collaterale": f"${usd:,.0f}" if usd else "?",
"azione": "valutare riconversione — la quota e' il controllo (N4)"})
inviati.append("DEPEG_CRIT")
if ok:
allertati.add("DEPEG_CRIT")
if "DEPEG_WARN" in nuove:
notify("⚠️ USDE sotto la pari", {"indice": px, "soglia": c["depeg_warn"],
"nota": "entra gia' nel sizing orario del book"})
ok = notify("⚠️ USDE sotto la pari", {"indice": px, "soglia": c["depeg_warn"],
"nota": "entra gia' nel sizing orario del book"})
inviati.append("DEPEG_WARN")
if ok:
allertati.add("DEPEG_WARN")
if "QUOTA_OVER" in nuove:
notify("⚠️ quota USDE sopra il tetto", {"quota": f"{quota:.1%}" if quota else "?",
"tetto": f"{c['quota_max_frac']:.0%}",
"nota": "anche una deriva passiva conta (il libro perde -> quota sale)"})
ok = notify("⚠️ quota USDE sopra il tetto", {"quota": f"{quota:.1%}" if quota else "?",
"tetto": f"{c['quota_max_frac']:.0%}",
"nota": "anche una deriva passiva conta (il libro perde -> quota sale)"})
inviati.append("QUOTA_OVER")
if ok:
allertati.add("QUOTA_OVER")
if "BLIND" in nuove:
notify("⚠️ usde_watch BLIND", {"motivo": motivo_blind or "?",
"nota": "'non vedo' non e' 'va tutto bene' (P5)"})
ok = notify("⚠️ usde_watch BLIND", {"motivo": motivo_blind or "?",
"nota": "'non vedo' non e' 'va tutto bene' (P5)"})
inviati.append("BLIND")
if ok:
allertati.add("BLIND")
rec["allertati"] = sorted(allertati)
v_prev = prev.get("verdetto") if prev else "IN_ATTESA"
if v != v_prev:
if v == "IDONEO":
notify("📌 USDE: conto IDONEO ai reward", {"dettaglio": v_motivo,
"prossimo passo": "decisione di quota dell'operatore (tetto allerta 50%)"})
"prossimo passo": f"decisione di quota dell'operatore (tetto allerta {c['quota_max_frac']:.0%})"})
elif v == "NON_IDONEO":
notify("⚠️ USDE: conto NON idoneo", {"dettaglio": v_motivo})
inviati.append(f"VERDETTO->{v}")
# La QUOTA e' dell'operatore ed e' stata rinviata a lunedi': da quel giorno lo si dice OGNI
# giorno, non solo alla transizione. Ripetersi qui e' voluto — non e' un allarme che chiede
# un'azione impossibile (P14), e' una domanda che da lunedi' si puo' finalmente rispondere,
# con accanto il numero per rispondere. Si smette togliendo `DECISIONE_QUOTA_DAL`.
rend = rendimento(records + [rec])
if quota_da_decidere(now, v):
apr = "n/d" if rend["apr"] is None else f"{rend['apr']*100:.2f}% annuo"
notify("📌 USDE: la decisione di QUOTA e' dovuta", {
"quota attuale": f"{quota:.1%}" if quota is not None else "n/d",
"tetto di allerta": f"{c['quota_max_frac']:.0%} (mai 100%: R1 emittente non recuperabile)",
"rendimento osservato": f"{apr} — {rend['nota']}, {rend['giorni']:.1f} giorni",
"nota": "un pagamento non e' un tasso: leggere l'APR con il suo n"})
inviati.append("QUOTA_DA_DECIDERE")
STATE.parent.mkdir(parents=True, exist_ok=True)
with STATE.open("a") as fh:
fh.write(json.dumps(rec) + "\n")
cambiato = bool(inviati) or v != v_prev
if not quiet or cambiato:
@@ -302,17 +349,16 @@ def main() -> int:
print(f" equity USDE : {eq_usde:,.4f} @ {px if px is not None else 'n/d'}"
f" ({px_fonte}) -> ${usd:,.2f}" if usd is not None else " equity USDE : n/d")
if quota is not None:
# Il conto e' Segregated Standard Margin (letto sulla pagina margini il 31/08):
# l'USDE NON fa margine per i perp USDC-settled, e l'haircut non si applica. Il
# margine del book e' il solo silo USDC. Si stampa anche cosa DAREBBE il cross,
# perche' e' la differenza che rende la cosa una decisione invece che un dettaglio.
# Il modello di margine ATTIVO e' Cross X:SM dal 2026-08-31 (decisione
# dell'operatore, config._nota_usde): l'USDE fa margine, scontato dell'haircut —
# NON e' piu' il Segregated Standard Margin di prima di quella data.
cross = eq_usdc + usd * (1.0 - c["haircut"])
print(f" quota : {quota:.1%} del totale (tetto allerta {c['quota_max_frac']:.0%})")
print(f" margine : ~${eq_usdc:,.0f} — il solo USDC (conto SEGREGATO: l'USDE non"
f" fa margine). Con cross X:SM sarebbero ~${cross:,.0f} (haircut"
print(f" margine : ~${cross:,.0f} USDC + USDE scontato (cross X:SM, haircut"
f" {c['haircut']:.0%})")
if an["delta"] is not None:
attr = ("non attribuibile (trade non leggibili)" if an["reward_stimato"] is None
attr = (f"non attribuibile (trade non leggibili: {trades_motivo})"
if an["reward_stimato"] is None
else f"reward stimato {an['reward_stimato']:+.6f} USDE"
+ (" [con trade nel mezzo]" if an["con_trade"] else ""))
print(f" delta : {an['delta']:+.6f} USDE dall'ultima lettura -> {attr}")
@@ -323,12 +369,20 @@ def main() -> int:
f" (tot {rend['tot']:+.6f} USDE)")
if rend["finestre"] < 4:
print(f" ⚠️ n={rend['finestre']}: un pagamento non e' un tasso")
if v == "IDONEO" and not quota_da_decidere(now, v):
print(f" quota : decisione RINVIATA a {DECISIONE_QUOTA_DAL} (operatore)")
if inviati:
print(f" allarmi : {', '.join(inviati)}")
return 0
USO = """uso: usde_watch.py [--quiet]
usde_watch.py — sorveglianza giornaliera del collaterale USDE: reward, depeg, quota. SOLA LETTURA.
uv run python scripts/live/usde_watch.py # report completo
uv run python scripts/live/usde_watch.py --quiet # stampa/allerta solo transizioni
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("usde_watch.py", USO, flag=("--quiet",), con_valore=())
sys.exit(main())
+3
View File
@@ -40,10 +40,12 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify # noqa: E402
FEED = "https://insights.deribit.com/exchange-updates/feed/"
STATE = ROOT / "data" / "live" / "venue_news.jsonl"
RUNS = ROOT / "data" / "venue_news" / "runs.jsonl"
# Parole che alzano un annuncio a 🚨. Le prime sono FISSE (categorie di rischio che ci toccano
# per costruzione); le altre si DERIVANO dal codice, sotto.
@@ -140,6 +142,7 @@ def main() -> int:
v["seed"] = primo_giro
f.write(json.dumps(v) + "\n")
nuove.append(v)
registra_giro(RUNS, voci=len(tutte), nuove=len(nuove))
# Primo giro: si semina lo storico SENZA allertare (P9: l'allarme massimo non si spende
# per un arretrato di 10 voci, o non verra' letto il giorno che e' vero).
+16
View File
@@ -16,10 +16,13 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.deribit import check_specs # noqa: E402
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify, ultimo_errore # noqa: E402
from src.live.venue_watch import (PERSIST_HOURS, THRESHOLD_BPS, # noqa: E402
run_once)
RUNS = ROOT / "data" / "venue_watch" / "runs.jsonl"
def _titolo(rep: dict) -> str:
"""⚠️ Il titolo dice cosa FARE, non solo cosa e' successo: chi lo legge sul telefono deve
@@ -28,6 +31,8 @@ def _titolo(rep: dict) -> str:
un blocco da manutenzione annunciata: il 18/08 sono usciti quattro messaggi identici che
chiedevano di prelevare, tutti dopo che il book aveva gia' ripreso. Un allarme massimo speso
per un evento atteso e' un allarme che non verra' letto il giorno che e' vero."""
if rep.get("severity") == "blind":
return "⚠️ venue_watch CIECO — referenze non leggibili, nessun giudizio sul venue"
if rep.get("severity") == "warn":
return "⚠️ VENUE WATCH — Deribit non operativa, ma spiegata. Nessuna azione"
return "🚨 VENUE WATCH — possibile stress su Deribit. PRIMO PASSO: prelievo di prova"
@@ -70,6 +75,7 @@ def main() -> int:
# che manda ordini. Il 18/08 Deribit ha cambiato tick e size dei perpetual lineari USDC e
# niente se n'era accorto: e' andata bene perche' i cambi erano riduzioni.
spec = check_specs()
registra_giro(RUNS, livelli=rep["levels"])
if spec["divergenze"]:
rifiuto = [d for d in spec["divergenze"] if d["rischio"] == "rifiuto"]
righe = {f"{d['strumento']} {d['campo']}": f"dichiarato {d['dichiarato']} / "
@@ -98,5 +104,15 @@ def main() -> int:
return 0
USO = """uso: venue_watch.py [--quiet]
venue_watch.py — runner ORARIO del tripwire di venue. Allerta su Telegram, NON blocca nulla.
uv run python scripts/live/venue_watch.py # un giro, stampa il report
uv run python scripts/live/venue_watch.py --quiet # solo in caso di allarme (per il cron)
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("venue_watch.py", USO, flag=("--quiet",), con_valore=())
raise SystemExit(main())
+7
View File
@@ -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'
@@ -48,9 +51,11 @@ sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
from cblib import ( # noqa: E402
dvol_series, f_factors, load_chain, pick_legs, puts, spot_series,
)
from src.live.giri import registra_giro # noqa: E402
from src.live.notifier import notify # noqa: E402
STATE = PROJECT_ROOT / "data" / "vrp_f_watch" / "state.json"
RUNS = PROJECT_ROOT / "data" / "vrp_f_watch" / "runs.jsonl"
# --- config CONGELATA il 2026-07-30 ---------------------------------------
STRUCTS = {
@@ -187,6 +192,8 @@ def main() -> int:
a["notified"] = bool(prev.get("notified", False))
_save_state(a)
if not R.empty:
registra_giro(RUNS, pairs=a["pairs"], ready=a["ready"])
return 0
+31 -2
View File
@@ -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
+20 -12
View File
@@ -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.
+11 -10
View File
@@ -17,7 +17,7 @@ SOLA LETTURA. Questo script:
- non importa nulla che invii ordini se non per ISPEZIONARLO (la sottoclasse di §1 ha `_post` che
SOLLEVA: se il codice di produzione toccasse la rete, il test fallirebbe rumorosamente);
- tocca il venue solo con `--venue`, solo `get_open_orders` (lettura), 2 richieste, e si rifiuta
di partire nei minuti :05-:10 (cron_book) e :24-:30 (cron_chain).
di partire nei minuti :45-:50 (cron_book, al :47 dal 25/08) e :24-:30 (cron_chain).
uv run python scripts/research/r0823_sl_anchor.py # attesa + log + simulazione
uv run python scripts/research/r0823_sl_anchor.py --venue # + riscontro sul conto (2 letture)
@@ -283,8 +283,8 @@ def riscontro_venue(runs_info: dict):
diritte di Deribit appartengono a un ALTRO progetto: usarle sarebbe inventare un percorso, e la
domanda non ne ha bisogno. Si usa quindi cio' che il percorso sanzionato offre: `get_open_orders`.""")
m = datetime.now(timezone.utc).minute
if 5 <= m <= 10 or 24 <= m <= 30:
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:07) / cron_chain (:25). Non interrogo.")
if 45 <= m <= 50 or 24 <= m <= 30: # cron_book al :47 dal 2026-08-25 (era :07), cron_chain :25
print(f"\n ⏸ minuto :{m:02d} — finestra di cron_book (:47) / cron_chain (:25). Non interrogo.")
return
from src.live.deribit import DeribitRead
r = DeribitRead()
@@ -395,10 +395,11 @@ def conseguenza():
alta dopo una salita, lo stop siede vicino sotto il mercato, il crollo lo prende, si rientra e
la gamba successiva lo riprende. **Il rischio di accumulo e' funzione della CADENZA DEL CRON,
non di k**: il gradino di leva non lo crea e non lo amplifica.
· ⚠️ E qui c'e' un aggancio operativo: il docstring di `scripts/live/book_execute.py` prescrive
«ogni ~230 minuti» (la griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443
giri in 1442 ore). Chi "correggesse" la cadenza verso il docstring sposterebbe il libro dalla
riga 1h alla riga 4h — quella in cui BTC raddoppia gli scatti.
· ⚠️ Aggancio operativo (RIPARATO il 2026-09-02, `tests/test_book_cadenza.py`): fino a quel
giorno il docstring di `scripts/live/book_execute.py` prescriveva «ogni ~230 minuti» (la
griglia di SKH01) mentre il cron gira **ogni ora** (misurato sopra: 1443 giri in 1442 ore).
Chi avesse "corretto" il CRON verso il docstring avrebbe spostato il libro dalla riga 1h
(rotolante: 0 scatti) alla riga 4h (2 scatti del rotolante contro 1 del pavimento).
(*) ⚠️ CAVEAT sulla colonna, e sul suo artefatto: in questa lente la posizione e' SEMPRE aperta
dal 2018, quindi in modo `roll` l'"ingresso" e' un prezzo di anni prima e il -48%/-61% e' il
@@ -501,9 +502,9 @@ def costo(out: dict):
⚠️ E c'e' un accoppiamento nuovo da registrare: l'accumulo di episodi e' funzione della CADENZA
DEL CRON (1h: no · 4h: BTC 2 · 24h: ETH 5 con 2 in 30 giorni). Oggi la cadenza e' misurata sana,
ma nessun sorvegliante la controlla contro questa conseguenza, e il docstring di `book_execute`
ne prescrive una (~230 min) che cade nella riga peggiore. Non e' un'azione richiesta qui: e' il
parametro da rileggere prima del gradino, insieme a G2.""")
e dal 2026-09-02 `tests/test_book_cadenza.py` tiene d'accordo docstring, cron_book.sh e crontab
(il docstring ne prescriveva una, ~230 min, nella riga 4h). Resta il parametro da rileggere prima
del gradino, insieme a G2.""")
def main():
+2 -1
View File
@@ -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()
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python
"""r0906_usde_tetto_sonda.py — sonda il TETTO del venue sull'USDE a SALDO NEUTRO, passi da 1-2 USDE.
INVIA ORDINI VERI (spot USDE_USDC, fee 0, ~$0,001 di spread per ordine).
Metodo (diari 30-31/08, sezione «la lezione di metodo»): un probe unico di taglia sbagliata produce un
falso negativo che si legge come risultato. Qui si sale a passi piccoli fino al primo rifiuto, si
ripete il rifiuto dopo aver riletto il book (per escludere l'ask che si e' mosso: il messaggio del
venue e' lo stesso `not_enough_funds_in_currency`), poi si CONFERMA a saldo neutro:
SELL 1 -> BUY 1 (deve passare) -> BUY 1 (deve essere rifiutato). Il bracket e' [L, L+1) USDE, con
l'equity totale letta nello stesso istante, cosi' la frazione ha la sua banda.
Guardie: prezzo dal BOOK (ask+5 tick, tetto 1,0010 / bid-5 tick, pavimento 0,9990), banda
[0,995, 1,005]; al massimo MAX_ADD USDE in piu' (quota mai oltre QUOTA_STOP); pausa fra ordini.
uv run python scripts/research/r0906_usde_tetto_sonda.py # dry-run: stato e piano
uv run python scripts/research/r0906_usde_tetto_sonda.py --esegui # sonda davvero
"""
from __future__ import annotations
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "live"))
from src.live import usde as U # noqa: E402
from src.live.deribit import DeribitRead # noqa: E402
import usde_convert as C # noqa: E402 (book_top, leggi_stato: stessi attrezzi)
LOG = ROOT / "data" / "live" / "usde_convert.jsonl"
PX_MIN, PX_MAX = C.PX_MIN, C.PX_MAX
TICK, MARGINE, CAP_BUY, CAP_SELL = 0.0001, 5, 1.0010, 0.9990
MAX_ADD = 60 # USDE in piu' al massimo (--max-add N): oltre, «tetto non trovato entro il limite»
QUOTA_HARD = 0.70 # mai oltre: e' la quota decisa dall'operatore il 30/08 (`usde.quota_target`), e coincide
# col CUSCINO DI REGOLAMENTO derivato da config (usde_convert.cuscino_richiesto_usd),
# che qui e' la guardia che morde davvero: si legge a ogni passo, non si ridichiara.
SCALA = [100.0, 20.0, 4.0, 1.0] # passi: si scende di uno a ogni doppio rifiuto (M13: risoluzione dell'effetto)
PAUSA, PAUSA_RIFIUTO = 12.0, 20.0
def stato(c: DeribitRead) -> dict:
eq_usdc, eq_usde, px, fonte = C.leggi_stato(c)
val, _ = U.valuta(eq_usde, px)
return {"usdc": eq_usdc, "usde": eq_usde, "px": px, "fonte": fonte, "val": val, "tot": eq_usdc + val,
"quota": val / (eq_usdc + val) if (eq_usdc + val) > 0 else None}
def prezzo_ordine(side: str) -> float | None:
bid, ask = C.book_top()
if side == "buy":
return None if ask is None else round(min(ask + MARGINE * TICK, CAP_BUY), 4)
return None if bid is None else round(max(bid - MARGINE * TICK, CAP_SELL), 4)
def ordine(c: DeribitRead, side: str, amt: float, px: float) -> tuple[bool, str]:
resp = c._unwrap(c._post("/mcp-deribit/tools/place_order",
{"instrument_name": U.SPOT, "side": side, "amount": float(amt),
"type": "limit", "price": px, "label": "usde-sonda-0906"})) or {}
if not isinstance(resp, dict) or resp.get("error") or "order" not in resp:
err = resp.get("error", resp) if isinstance(resp, dict) else resp
return False, str(err)[:120]
o = resp["order"]
return True, f"{o.get('order_state')} filled {o.get('filled_amount')}"
def main() -> int:
esegui = "--esegui" in sys.argv[1:]
max_add = float(sys.argv[sys.argv.index("--max-add") + 1]) if "--max-add" in sys.argv else MAX_ADD
c = DeribitRead()
s0 = stato(c)
if s0["px"] is None:
print(" ✗ prezzo USDE non leggibile — non si sonda al buio (P5)"); return 2
print("=" * 78)
print(f" SONDA TETTO USDE — {datetime.now(timezone.utc):%Y-%m-%dT%H:%M:%SZ} "
f"{'ESECUZIONE' if esegui else 'DRY-RUN'}")
print("=" * 78)
cap = U.cfg().get("venue_cap_frac")
print(f" conto : USDC ${s0['usdc']:,.2f} + USDE {s0['usde']:,.4f} (${s0['val']:,.2f}) = ${s0['tot']:,.2f}"
f" quota {s0['quota']:.2%} · tetto in config {cap:.1%} = {cap * s0['tot']:,.1f} USDE")
cusc, come = C.cuscino_richiesto_usd(s0["tot"])
print(f" piano : BUY a passi {'→'.join(f'{x:.0f}' for x in SCALA)} fino al rifiuto (max +{max_add:.0f} USDE;"
f" quota ≤{QUOTA_HARD:.0%} e USDC ≥ cuscino ${cusc:,.2f} = {come}), conferma SELL 1 → BUY 1 ok → BUY 1 rifiutato")
if not esegui:
print(" → dry-run. Rilancia con --esegui."); return 0
eventi: list[dict] = []
def fai(side: str, amt: float) -> bool:
px = prezzo_ordine(side)
if px is None or not (PX_MIN <= px <= PX_MAX):
print(f" ✗ prezzo d'ordine {px} non leggibile/fuori banda — mi fermo"); raise SystemExit(3)
ok, nota = ordine(c, side, amt, px)
time.sleep(3.0)
s = stato(c)
ev = {"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "side": side, "amount": amt,
"px": px, "ok": ok, "nota": nota, "usde": s["usde"], "tot": s["tot"], "quota": s["quota"]}
eventi.append(ev)
print(f" · {side.upper():4} {amt:>3.0f} @ {px:.4f} -> {'OK ' if ok else 'RIFIUTATO'} "
f"USDE {s['usde']:,.4f} eq ${s['tot']:,.2f} quota {s['quota']:.3%} [{nota}]")
return ok
# --- salita a passi piccoli -------------------------------------------------------------
i_step, aggiunti, muro, limite = 0, 0.0, None, None
while True:
step = SCALA[i_step]
s = stato(c)
cusc, _ = C.cuscino_richiesto_usd(s["tot"])
if aggiunti + step > max_add:
limite = f"+{aggiunti:.0f} USDE raggiunti (max-add {max_add:.0f})"; break
if (s["val"] + step * s["px"]) / s["tot"] > QUOTA_HARD:
limite = f"quota {s['quota']:.2%} + passo supererebbe {QUOTA_HARD:.0%}"; break
if s["usdc"] - step * s["px"] < cusc:
limite = f"USDC ${s['usdc']:,.2f} − passo scenderebbe sotto il cuscino ${cusc:,.2f}"; break
if fai("buy", step):
aggiunti += step; time.sleep(PAUSA); continue
time.sleep(PAUSA_RIFIUTO) # rifiuto: rileggo il book e riprovo UGUALE
if fai("buy", step):
aggiunti += step; time.sleep(PAUSA); continue
if i_step < len(SCALA) - 1:
i_step += 1; time.sleep(PAUSA); continue # doppio rifiuto: passo piu' fine
muro = stato(c); break
if limite:
s = stato(c)
print(f" ✗ limite della sonda: {limite} — tetto del venue NON trovato entro il limite;"
f" quota {s['quota']:.2%}, USDE {s['usde']:,.4f}")
if muro is None:
s = stato(c)
esito = {"verdetto": "NON_TROVATO", "limite": limite, "usde": s["usde"], "equity_tot": s["tot"],
"quota": s["quota"], "aggiunti": aggiunti, "eventi": eventi}
else:
# --- conferma a saldo neutro ---------------------------------------------------------
time.sleep(PAUSA)
ok_sell = fai("sell", 1); time.sleep(PAUSA)
ok_buy1 = fai("buy", 1) if ok_sell else False; time.sleep(PAUSA)
ok_buy2 = fai("buy", 1) if ok_buy1 else None; time.sleep(3.0)
fin = stato(c)
confermato = bool(ok_sell and ok_buy1 and ok_buy2 is False)
L = fin["usde"]
lo, hi = L / fin["tot"], (L + 1) / fin["tot"]
esito = {"verdetto": "CONFERMATO" if confermato else "NON_CONFERMATO",
"bracket_usde": [L, L + 1], "equity_tot": fin["tot"], "frazione": [lo, hi],
"aggiunti": aggiunti, "eventi": eventi}
print()
print(f" TETTO : [{L:,.4f} · {L + 1:,.4f}) USDE con equity ${fin['tot']:,.2f}"
f" ⇒ frazione {lo:.2%} – {hi:.2%} ({esito['verdetto']}: "
f"sell1 {ok_sell} / buy1 {ok_buy1} / buy1 {ok_buy2})")
print(f" config : venue_cap_frac {cap:.3f} → bordo basso misurato oggi {lo:.4f}"
+ (" (config sotto il misurato: conservativa, ok)" if cap <= lo + 1e-9 else " ⚠️ config SOPRA il misurato"))
print(f" conto : USDC ${fin['usdc']:,.2f} + USDE {fin['usde']:,.4f} = ${fin['tot']:,.2f} quota {fin['quota']:.2%}")
with LOG.open("a") as f:
f.write(json.dumps({"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"sonda_tetto": esito}) + "\n")
print(f" registrato : {LOG.relative_to(ROOT)}")
return 0 if esito["verdetto"] == "CONFERMATO" else 1
if __name__ == "__main__":
raise SystemExit(main())
+384
View File
@@ -0,0 +1,384 @@
"""r0909 — IL CROLLO CATTURATO: il `f` di stress delle opzioni, misurato su quote VERE.
PERCHE' ESISTE. La decisione vincolante del 19/06 ("niente short-vol da modello in deploy", e la
gemella "niente long-vol scalp") dichiara UNA condizione che la riapre: *un `f` di stress reale
misurato su un crash catturato*. COLLAR01 (§71) e PAVIMENTO-LEVA (§72) chiudono con la stessa
condizione. Il 22/08 (§11) il campione con quote vere era "la strategia mentre STA FERMA":
19 settimane di rally, "l'unico episodio ad alta vol del campione e' un rally, non un crash".
FATTO NUOVO (2026-09-09): l'archivio ereditato `bite_archive.parquet` copre con bid E ask
il crollo del 1-5 giugno 2026 — BTC −25% e ETH −30% in 5 giorni (peggior giorno ETH −10,6%
il 05/06), su UNA scadenza (19 giugno, 14-21 DTE agli ingressi). Il campione del 22/08
partiva DOPO, dal fondo. Nessuno lo aveva usato: e' il crash catturato che la regola aspettava.
COSA MISURA (a ogni ora d'ingresso dal 30/05 al 04/06, BTC e ETH — l'ora e' un'ancora, si
riportano mediana e banda, mai il massimo):
A. la geometria di VRP01 (vendi put δ−0,28, compra put δ−0,10, stessa scadenza) a quote REALI:
credito al bid/ask, mark-to-market ora per ora dalle standing quotes, regolamento a scadenza
sullo spot certificato. Contro lo STESSO trade prezzato dal modello del sleeve (BS su DVOL,
`sleeves._bs_put`). f_stress = perdita reale / perdita modellata, a scadenza e al peggior MTM.
B. lo specchio LONG (compra la put δ−0,10 all'ask; e il debit spread −0,28/−0,10): quanto del
payoff modellato si incassa davvero al bid — a scadenza (uscita passiva) e al miglior punto
del percorso (limite superiore con look-ahead, dichiarato).
C. il contesto: DVOL, RV realizzata, IV-rank causale (il gate di VRP01 era aperto o chiuso?),
e cosa dice il modello del sleeve (`_vrp_weekly_asset`) per le settimane di giugno.
LIMITI DICHIARATI PRIMA DEI NUMERI: n = 1 crollo, 1 scadenza per asset, tenor 14-21 giorni
(VRP01 e' a 7), 2-5 mila righe/giorno (40-125 strumenti/ora), `underlying_price` assente
nell'archivio (spot dal feed 1h certificato, D1), regolamento approssimato allo spot dell'ora
di scadenza (Deribit usa una TWAP di 30 min). La famiglia dell'archivio e' INVERSE (premi in
BTC/ETH; `ETH-19JUN26-1900-P`): VRP01 opererebbe sulla USDC-lineare, che il conto puo' marginare
e che NON e' nell'archivio (§11) — il f qui misurato e' della famiglia che il conto non negozia.
Un episodio e' un episodio: da' la TAGLIA di f nello stress, non la sua distribuzione.
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. 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
tagliate sono contate. Senza il taglio il "peggior MTM" era un artefatto di quote (2,6×).
* `f_exp` e' TAUTOLOGICO quando entrambe le gambe finiscono ITM (payoff = larghezza per reale
e modello): si riduce a una funzione di f_net. Si stampa la quota di trade in cui succede.
Nessun file di produzione toccato. Nessun ordine.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import cblib as CB # noqa: E402
from src.portfolio.sleeves import VRP_CFG, _vrp_weekly_asset # noqa: E402
ARCHIVIO = ROOT / "data" / "raw" / "cb_chain" / "bite_archive.parquet"
ASSETS = ("BTC", "ETH")
EXP = pd.Timestamp("2026-06-19 08:00", tz="UTC")
INGRESSI = (pd.Timestamp("2026-05-30", tz="UTC"), pd.Timestamp("2026-06-05", tz="UTC")) # [da, a)
CROLLO = (pd.Timestamp("2026-06-01", tz="UTC"), pd.Timestamp("2026-06-06", tz="UTC"))
PRE = pd.Timestamp("2026-06-02", tz="UTC") # ingressi "prima del crollo" = prima del primo giorno a −7%/−8% (il 01/06 BTC fece −3,1%: e' DENTRO "pre", dichiarato)
TOL_D = (0.08, 0.06) # geometria VRP01: |δ corta + 0,28| ≤ 0,08 e |δ lunga + 0,10| ≤ 0,06, altrimenti "fuori geometria"
SHORT_D, LONG_D = VRP_CFG["short_delta"], VRP_CFG["long_delta"] # P1: dal sleeve, non ridichiarati
FEE_FRAC = VRP_CFG["fee_frac"]
# listino Deribit opzioni (stesse costanti di r0822_vrp_real_quotes, verificate al venue il 22/08)
FEE_UNDERLYING, FEE_PREMIUM_CAP, FEE_DELIVERY = 0.0003, 0.125, 0.00015
def leg_fee(prem_base: float, spot: float) -> float:
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot)
def carica() -> pd.DataFrame:
a = pd.read_parquet(ARCHIVIO)
a["ts"] = pd.to_datetime(a["ts"], utc=True)
a["exp"] = pd.to_datetime(a["exp"], utc=True)
a = a[(a["option_type"] == "P") & (a["exp"] == EXP) & (a["ts"] >= INGRESSI[0]) & (a["ts"] <= EXP)]
a = a.drop_duplicates(subset=["ts", "instrument_name"], keep="last")
a["dte"] = (a["exp"] - a["ts"]).dt.total_seconds() / 86400.0
return a.reset_index(drop=True)
def bs_spread(S: float, Ks: float, Kl: float, T: float, sig: float) -> float:
return CB.bs_put(S, Ks, T, sig) - CB.bs_put(S, Kl, T, sig)
def bs_put_vec(S, K, T, sig):
"""`cblib.bs_put` vettorizzata (stessa formula, r=0); T≤0 → intrinseco."""
from scipy.stats import norm
S = np.asarray(S, float); T = np.asarray(T, float); sig = np.asarray(sig, float)
out = np.maximum(K - S, 0.0)
ok = (T > 0) & (sig > 0)
d1 = (np.log(S[ok] / K) + 0.5 * sig[ok] ** 2 * T[ok]) / (sig[ok] * np.sqrt(T[ok]))
out[ok] = K * norm.cdf(-(d1 - sig[ok] * np.sqrt(T[ok]))) - S[ok] * norm.cdf(-d1)
return out
class Quote:
"""Tabella ora × strumento (bid, ask, iv) costruita UNA volta per asset, con spot e DVOL per ora."""
def __init__(self, seg: pd.DataFrame, S: pd.Series, V: pd.Series):
q = seg.dropna(subset=["bid", "ask"]); q = q[(q["bid"] > 0) & (q["ask"] > 0)]
self.bid = q.pivot_table(index="ts", columns="instrument_name", values="bid")
self.ask = q.pivot_table(index="ts", columns="instrument_name", values="ask")
self.iv = q.pivot_table(index="ts", columns="instrument_name", values="iv")
idx = self.bid.index
self.spot = pd.Series([float(S.asof(t)) for t in idx], index=idx)
self.dvol = pd.Series([float(V.asof(t)) for t in idx], index=idx) / 100.0
self.T = pd.Series(np.maximum((EXP - idx).total_seconds().values / 86400.0, 0.0) / 365.25, index=idx)
def percorso(Q: Quote, dopo: pd.Timestamp, inst_s: str, inst_l: str, Ks: float, Kl: float) -> pd.DataFrame:
"""Ora per ora dopo l'ingresso: costo REALE di chiudere lo short spread (ricompra la corta
all'ask, rivende la lunga al bid), valore REALE di liquidare il debit spread (vende la corta
al bid, ricompra la lunga all'ask) e la put lunga (bid); piu' il valore MODELLATO (BS a DVOL_t)."""
if inst_s not in Q.bid.columns or inst_l not in Q.bid.columns:
return pd.DataFrame()
m = (Q.bid.index > dopo) & Q.bid[inst_s].notna() & Q.bid[inst_l].notna()
if not m.any():
return pd.DataFrame()
idx = Q.bid.index[m]
sp = Q.spot.loc[idx].values; dv = Q.dvol.loc[idx].values; T = Q.T.loc[idx].values
bs_, as_ = Q.bid.loc[idx, inst_s].values, Q.ask.loc[idx, inst_s].values
bl, al = Q.bid.loc[idx, inst_l].values, Q.ask.loc[idx, inst_l].values
mod_s, mod_l = bs_put_vec(sp, Ks, T, dv), bs_put_vec(sp, Kl, T, dv)
grezzo = (as_ - bl) * sp
return pd.DataFrame(dict(ts=idx, spot=sp, dvol=dv,
chiudi_short=np.minimum(grezzo, Ks - Kl), chiudi_grezzo=grezzo,
liquida_long=np.clip((bs_ - al) * sp, 0.0, Ks - Kl), liquida_put=bl * sp,
mod_spread=mod_s - mod_l, mod_put=mod_l,
iv_s=Q.iv.loc[idx, inst_s].values, iv_l=Q.iv.loc[idx, inst_l].values))
def trade(asset: str, ts: pd.Timestamp, snap: pd.DataFrame, Q: Quote,
S: pd.Series, V: pd.Series) -> dict | None:
legs = CB.pick_legs(snap, SHORT_D, LONG_D)
if legs is None:
return None
spot = float(S.asof(ts)); dvol = float(V.asof(ts)); T = (EXP - ts).total_seconds() / 86400.0 / 365.25
hist = V[V.index < ts]; ivr = float((hist < dvol).mean()) if len(hist) else np.nan
Ks, Kl = legs["k_short"], legs["k_long"]
# --- A. short spread (VRP01) — reale al bid/ask, modello a DVOL
cred = (legs["bid_short"] - legs["ask_long"]) * spot
if cred <= 0:
return None
cred_mod = bs_spread(spot, Ks, Kl, T, dvol / 100.0)
ST = float(S.asof(EXP))
payoff = max(Ks - ST, 0.0) - max(Kl - ST, 0.0)
fee_in = leg_fee(legs["bid_short"], spot) + leg_fee(legs["ask_long"], spot)
fee_out = sum(min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * max(K - ST, 0.0)) for K in (Ks, Kl) if K > ST)
pnl_real = cred - payoff - fee_in - fee_out
pnl_mod = cred_mod - payoff - FEE_FRAC * cred_mod
p = percorso(Q, ts, legs["inst_short"], legs["inst_long"], Ks, Kl)
if p.empty:
return None
mtm_real = cred - p["chiudi_short"] # P&L aperto se si chiude ORA a quote vere (tagliato alla larghezza)
mtm_mod = cred_mod - p["mod_spread"] # lo stesso trade riprezzato a BS/DVOL_t (costruzione nostra: il sleeve tiene a scadenza)
i_min = int(mtm_real.idxmin())
n_tagli = int((p["chiudi_grezzo"] > Ks - Kl).sum())
# --- B. specchio long: put δ−0,10 comprata all'ask; debit spread −0,28/−0,10
costo_put = legs["ask_long"] * spot
mod_put = CB.bs_put(spot, Kl, T, dvol / 100.0)
costo_deb = (legs["ask_short"] - legs["bid_long"]) * spot
mod_deb = cred_mod
pay_put_exp = max(Kl - ST, 0.0)
pay_deb_exp = payoff
# il picco del percorso e' un limite SUPERIORE con look-ahead: si dichiara, non si vende
i_put = int(p["liquida_put"].idxmax()); i_deb = int(p["liquida_long"].idxmax())
risk = (Ks - Kl) - cred
in_geom = abs(legs["d_short"] - SHORT_D) <= TOL_D[0] and abs(legs["d_long"] - LONG_D) <= TOL_D[1]
return dict(
in_geom=bool(in_geom),
asset=asset, ts=ts, dte=(EXP - ts).total_seconds() / 86400.0, spot=spot, dvol=dvol, ivrank=ivr,
k_short=Ks, k_long=Kl, d_short=legs["d_short"], d_long=legs["d_long"], width=Ks - Kl,
iv_s0=legs["iv_short"], iv_l0=legs["iv_long"], ST=ST, und_ret=ST / spot - 1.0,
cred_real=cred, cred_mod=cred_mod, f_net=cred / cred_mod if cred_mod > 0 else np.nan,
risk=risk, pnl_real=pnl_real, pnl_mod=pnl_mod,
perdita_su_risk=-pnl_real / risk, perdita_su_coll=-pnl_real / Ks,
f_exp=(-pnl_real / -pnl_mod) if pnl_mod < 0 and pnl_real < 0 else np.nan,
mtm_real_min=float(mtm_real.min()), mtm_mod_min=float(mtm_mod.min()),
mtm_min_ts=p["ts"].iloc[i_min], n_tagli=n_tagli, n_ore=int(len(p)),
iv_s_at_min=float(p["iv_s"].iloc[i_min]), iv_l_at_min=float(p["iv_l"].iloc[i_min]), dvol_at_min=float(p["dvol"].iloc[i_min]) * 100,
itm_pieno=bool(ST <= Kl), # entrambe le gambe ITM: payoff = larghezza, f_exp si riduce a f_net
f_mtm=(mtm_real.min() / mtm_mod.min()) if mtm_mod.min() < 0 and mtm_real.min() < 0 else np.nan,
spot_min=float(p["spot"].min()), iv_s_max=float(p["iv_s"].max()), iv_l_max=float(p["iv_l"].max()),
n_ore_percorso=int(len(p)),
# long
costo_put=costo_put, mod_put=mod_put, f_put_in=costo_put / mod_put if mod_put > 0 else np.nan,
put_exp=pay_put_exp - costo_put, put_exp_mod=pay_put_exp - mod_put,
put_best=float(p["liquida_put"].iloc[i_put]) - costo_put, put_best_mod=float(p["mod_put"].iloc[i_put]) - mod_put,
put_best_ts=p["ts"].iloc[i_put], bleed_put=costo_put / spot,
costo_deb=costo_deb, mod_deb=mod_deb,
deb_exp=pay_deb_exp - costo_deb, deb_exp_mod=pay_deb_exp - mod_deb,
deb_best=float(p["liquida_long"].iloc[i_deb]) - costo_deb, deb_best_mod=float(p["mod_spread"].iloc[i_deb]) - mod_deb,
)
def misura() -> dict:
a = carica()
out = {"trades": {}, "contesto": {}, "sleeve": {}}
for asset in ASSETS:
seg = a[a["asset"] == 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()):
ts = pd.Timestamp(ts)
if not (INGRESSI[0] <= ts < INGRESSI[1]):
continue
t = trade(asset, ts, seg[seg["ts"] == ts], Q, S, V)
if t:
rows.append(t)
out["trades"][asset] = pd.DataFrame(rows)
# 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
# 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 = 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 < 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),
)
wk = _vrp_weekly_asset(asset)
out["sleeve"][asset] = wk[(wk.index >= pd.Timestamp("2026-05-10", tz="UTC")) & (wk.index <= pd.Timestamp("2026-07-05", tz="UTC"))]
return out
def sintesi(m: dict) -> dict:
s = {}
for asset in ASSETS:
t = m["trades"][asset]
if t.empty:
s[asset] = None; continue
n_fuori = int((~t["in_geom"]).sum())
t = t[t["in_geom"]] # la griglia di strike dell'archivio e' rada: fuori geometria = altra struttura
if t.empty:
s[asset] = dict(n=0, n_fuori=n_fuori); continue
pre = t[t["ts"] < PRE] # ingressi PRIMA del crollo: il caso della regola
dur = t[(t["ts"] >= PRE) & (t["ts"] < CROLLO[1] - pd.Timedelta(days=1))] # DURANTE (02-04/06): lente secondaria
med = lambda c, d=t: float(np.nanmedian(d[c])) if len(d) else np.nan
b = lambda c, d=t: (float(np.nanpercentile(d[c], 10)), float(np.nanpercentile(d[c], 90))) if len(d) else (np.nan, np.nan)
s[asset] = dict(
n=int(len(t)), n_pre=int(len(pre)), n_fuori=n_fuori,
d_short=med("d_short", pre), d_long=med("d_long", pre), width=med("width", pre), ST=float(t["ST"].iloc[0]),
spot_pre=med("spot", pre),
dte=(float(t["dte"].min()), float(t["dte"].max())),
f_net=med("f_net"), f_net_b=b("f_net"),
ivrank=(float(t["ivrank"].min()), float(t["ivrank"].max())),
gate_aperto=float((t["ivrank"] >= VRP_CFG["gate_ivr"]).mean()),
perdita_su_risk=med("perdita_su_risk", pre), perdita_su_risk_b=b("perdita_su_risk", pre),
perdita_su_coll=med("perdita_su_coll", pre),
pnl_real=med("pnl_real", pre), pnl_mod=med("pnl_mod", pre),
f_exp=med("f_exp", pre), f_exp_b=b("f_exp", pre), n_f_exp=int(pre["f_exp"].notna().sum()),
f_mtm=med("f_mtm", pre), f_mtm_b=b("f_mtm", pre),
mtm_real_min=med("mtm_real_min", pre), mtm_mod_min=med("mtm_mod_min", pre),
n_tagli=int(pre["n_tagli"].sum()) if len(pre) else 0, n_ore=int(pre["n_ore"].sum()) if len(pre) else 0,
iv_s_at_min=med("iv_s_at_min", pre), dvol_at_min=med("dvol_at_min", pre),
q_itm_pieno=float(pre["itm_pieno"].mean()) if len(pre) else np.nan,
n_strutture=int(pre.groupby(["k_short", "k_long"]).ngroups) if len(pre) else 0,
f_put_in=med("f_put_in", pre), bleed_put=med("bleed_put", pre),
put_exp=med("put_exp", pre), put_exp_mod=med("put_exp_mod", pre),
put_best=med("put_best", pre), put_best_mod=med("put_best_mod", pre),
f_put_exp=med_ratio(pre, "put_exp", "put_exp_mod"), f_put_best=med_ratio(pre, "put_best", "put_best_mod"),
deb_exp=med("deb_exp", pre), deb_exp_mod=med("deb_exp_mod", pre),
deb_best=med("deb_best", pre), deb_best_mod=med("deb_best_mod", pre),
f_deb_exp=med_ratio(pre, "deb_exp", "deb_exp_mod"), f_deb_best=med_ratio(pre, "deb_best", "deb_best_mod"),
iv_s0=med("iv_s0", pre), iv_s_max=med("iv_s_max", pre), iv_l0=med("iv_l0", pre), iv_l_max=med("iv_l_max", pre),
und_ret=med("und_ret", pre), spot_min=med("spot_min", pre),
durante=dict(n=int(len(dur)), f_exp=med("f_exp", dur), f_mtm=med("f_mtm", dur),
perdita_su_risk=med("perdita_su_risk", dur), f_net=med("f_net", dur),
q_perde=float((dur["pnl_real"] < 0).mean()) if len(dur) else np.nan,
pnl_p10=float(np.nanpercentile(dur["pnl_real"], 10)) if len(dur) else np.nan,
pnl_p90=float(np.nanpercentile(dur["pnl_real"], 90)) if len(dur) else np.nan,
d_short=med("d_short", dur), d_long=med("d_long", dur), width=med("width", dur),
pnl_real=med("pnl_real", dur), pnl_mod=med("pnl_mod", dur)),
)
return s
def med_ratio(d: pd.DataFrame, a: str, b: str, minimo: float = 5.0) -> float:
"""Mediana del rapporto reale/modello sulle righe in cui il modello vale almeno `minimo` $
(un payoff modellato ≈ 0 non da' un rapporto, da' un'altra domanda: si stampano i dollari)."""
x = d[(d[b] >= minimo)]
return float(np.nanmedian(x[a] / x[b])) if len(x) else np.nan
def verdetto(s: dict) -> str:
parti = []
for asset in ASSETS:
r = s.get(asset)
if not r or r.get("n", 0) == 0:
parti.append(f"{asset}: NON misurabile — {r.get('n_fuori', 0) if r else 0} snapshot tutti FUORI geometria (griglia di strike rada)"); continue
if not np.isfinite(r["f_exp"]):
parti.append(f"{asset}: nessun ingresso pre-crollo in geometria (solo DURANTE: f_net {r['durante']['f_net']:.2f}, perde il {r['durante']['q_perde']*100:.0f}%)"); continue
parti.append(f"{asset}: f_net {r['f_net']:.2f} (22/08 in rally 0,714); a scadenza f {r['f_exp']:.2f} ma TAUTOLOGICO nel "
f"{r['q_itm_pieno']*100:.0f}% dei trade (entrambe le gambe ITM ⇒ dipende solo da f_net; {r['n_strutture']} strutture, non {r['n_pre']} osservazioni); "
f"peggior MTM tagliato alla larghezza f {r['f_mtm']:.2f} (quote oltre la larghezza nel {r['n_tagli']/max(1, r['n_ore'])*100:.1f}% delle ore); "
f"gate IV-rank {'APERTO' if r['gate_aperto'] > 0.5 else 'CHIUSO'} nel {r['gate_aperto']*100:.0f}% delle ore")
esito = ("IL CROLLO E' CATTURATO MA A VOL BASSA E FUORI DAL GATE DI VRP01: il f di stress che la regola del 19/06 aspettava "
"NON e' misurato nella sostanza — cio' che e' misurato e' che il rapporto di credito f_net non cambia col regime")
return esito + " — " + "; ".join(parti) + " — n = 1 crollo, 1 scadenza per asset, famiglia INVERSE"
def pct(x): return f"{x*100:+.1f}%"
def main() -> None:
print("=" * 104)
print(" r0909 — IL CROLLO CATTURATO (1-5 giugno 2026): f di stress su quote VERE, scadenza 19/06")
print("=" * 104)
m = misura(); s = sintesi(m)
for asset in ASSETS:
c = m["contesto"][asset]; r = s[asset]
print(f"\n [{asset}] CONTESTO — caduta {pct(c['caduta'])} dal 31/05 al minimo di 1-5/06; "
f"DVOL pre {c['dvol_pre']:.1f} (IV-rank causale {c['ivr_pre']:.2f}) → max {c['dvol_max_crollo']:.1f} "
f"(= {c['pctl_picco']*100:.0f}° percentile storico: il PICCO del crollo sta sotto la mediana); RV5 max {c['rv5_max']:.0f}")
print(" giorno spot ret DVOL RV5")
for d in pd.date_range("2026-05-28", "2026-06-08", tz="UTC"):
if d in c["spot"].index:
dv = c["dvol"].asof(d)
print(f" {d.date()} {c['spot'][d]:>8.0f} {pct(c['ret'].get(d, np.nan)):>7} {dv:>5.1f} {c['rv5'].get(d, np.nan):>5.0f}")
if r is None or r.get("n", 0) == 0:
print(f" nessun trade in geometria VRP01: {r.get('n_fuori', 0) if r else 0} snapshot fuori geometria "
f"(la griglia di strike dell'archivio in quei giorni non ha le δ−0,28/−0,10)"); continue
t = m["trades"][asset]; t = t[t["in_geom"]]
print(f"\n [{asset}] A. VRP01 a quote vere — {r['n']} snapshot d'ingresso in geometria ({r['n_pre']} prima del {PRE.date()}; "
f"{r['n_fuori']} scartati fuori geometria), DTE {r['dte'][0]:.0f}-{r['dte'][1]:.0f}, IV-rank {r['ivrank'][0]:.2f}-{r['ivrank'][1]:.2f}")
print(f" geometria mediana pre-crollo: δ corta {r['d_short']:+.2f} / δ lunga {r['d_long']:+.2f}, larghezza {r['width']:.0f}, "
f"spot d'ingresso {r['spot_pre']:.0f}, regolamento {EXP.date()} a {r['ST']:.0f}")
print(f" credito reale/modello f_net {r['f_net']:.2f} [{r['f_net_b'][0]:.2f}, {r['f_net_b'][1]:.2f}] "
f"(22/08: 0,714 in rally)")
print(f" ingressi PRE-crollo, mediana: P&L reale ${r['pnl_real']:+.0f} vs modello ${r['pnl_mod']:+.0f} per unita' "
f"→ perdita {r['perdita_su_risk']*100:.0f}% del rischio [{r['perdita_su_risk_b'][0]*100:.0f}, {r['perdita_su_risk_b'][1]*100:.0f}] "
f"(100% = perdita MASSIMA dello spread; oltre 100 sono le fee), "
f"{r['perdita_su_coll']*100:.1f}% del collaterale")
print(f" f a SCADENZA {r['f_exp']:.2f} [{r['f_exp_b'][0]:.2f}, {r['f_exp_b'][1]:.2f}] su {r['n_f_exp']} snapshot = {r['n_strutture']} strutture "
f"— TAUTOLOGICO nel {r['q_itm_pieno']*100:.0f}% (entrambe le gambe ITM: payoff = larghezza per reale e modello, resta solo f_net)")
print(f" peggior MTM (chiusura tagliata alla larghezza; quote oltre la larghezza nel {r['n_tagli']/max(1, r['n_ore'])*100:.1f}% delle ore-percorso): "
f"f {r['f_mtm']:.2f} [{r['f_mtm_b'][0]:.2f}, {r['f_mtm_b'][1]:.2f}] (reale ${r['mtm_real_min']:+.0f} / modello ${r['mtm_mod_min']:+.0f}); "
f"a quell'ora IV corta {r['iv_s_at_min']:.0f} vs DVOL {r['dvol_at_min']:.0f}")
print(f" IV gamba corta {r['iv_s0']:.0f} → max {r['iv_s_max']:.0f} (il max e' a T→0, non nel crollo); gamba lunga {r['iv_l0']:.0f} → max {r['iv_l_max']:.0f}; "
f"sottostante a scadenza {pct(r['und_ret'])}, minimo {r['spot_min']:.0f}")
du = r["durante"]
print(f" lente secondaria — ingressi DURANTE il crollo (02-04/06, {du['n']} snapshot): δ {du['d_short']:+.2f}/{du['d_long']:+.2f}, "
f"larghezza {du['width']:.0f}, f_net {du['f_net']:.2f}, P&L reale ${du['pnl_real']:+.0f} vs modello ${du['pnl_mod']:+.0f}, "
f"perde nel {du['q_perde']*100:.0f}% (p10 ${du['pnl_p10']:+.0f} / p90 ${du['pnl_p90']:+.0f}: bimodale, la mediana nasconde le perdite), "
f"f scadenza {du['f_exp']:.2f} / peggior MTM {du['f_mtm']:.2f}")
print(f"\n [{asset}] B. LO SPECCHIO LONG (ingressi PRE-crollo, mediana per unita')")
print(f" put δ−0,10 all'ask: costo reale/modello {r['f_put_in']:.2f}, bleed {r['bleed_put']*100:.2f}% dello spot")
print(f" a scadenza: reale ${r['put_exp']:+.0f} vs modello ${r['put_exp_mod']:+.0f} → f_payoff {r['f_put_exp']:.2f}")
print(f" al PICCO del percorso (look-ahead, limite sup.): reale ${r['put_best']:+.0f} vs modello ${r['put_best_mod']:+.0f} → f {r['f_put_best']:.2f}")
print(f" debit spread −0,28/−0,10: a scadenza reale ${r['deb_exp']:+.0f} vs modello ${r['deb_exp_mod']:+.0f} → f {r['f_deb_exp']:.2f}; "
f"al picco ${r['deb_best']:+.0f} vs ${r['deb_best_mod']:+.0f} → f {r['f_deb_best']:.2f}")
print(f"\n [{asset}] C. IL MODELLO DEL SLEEVE (`_vrp_weekly_asset`, 0.0 = settimana saltata dai gate) — rendimento settimanale su collaterale:")
print(" " + " ".join(f"{d.date()} {v*100:+.2f}%" for d, v in m["sleeve"][asset].items()))
print(f"\n snapshot d'ingresso (primi 6): " + " | ".join(
f"{row.ts.strftime('%m-%d %H')}h K{row.k_short:.0f}/{row.k_long:.0f} cred ${row.cred_real:.0f} pnl ${row.pnl_real:+.0f} mtm ${row.mtm_real_min:+.0f}"
for row in t.head(6).itertuples()))
print("\n VERDETTO: " + verdetto(s))
print("=" * 104)
if __name__ == "__main__":
main()
+125
View File
@@ -0,0 +1,125 @@
"""r0909 — L'UNIVERSO DERIBIT LETTO OGGI: quali "altre monete" esistono davvero sul venue.
Domanda dell'operatore: "anche piu' monete, ma sempre in Deribit". Prima di misurare una
moneta si legge il venue (N10: la verifica esterna a €0 si fa PRIMA, e sul venue). Questo
script interroga l'API PUBBLICA Deribit (nessuna credenziale, nessun ordine) e stampa:
1. i perpetual USDC-lineari aperti, con data di listing, lotto minimo in $, volume 24h,
spread top-of-book, open interest e funding — ordinati per volume;
2. quante delle 19 gambe di XS01 sono quotate (il 23/08 erano 14/19, 11 con ≥1 anno);
3. le famiglie di opzioni (inverse BTC/ETH; USDC-lineari per sottostante).
Una lettura e' UN istante (come r0822_alt_options): il file salva il JSON grezzo in
`data/_cache/` con l'ora della lettura, cosi' il numero stampato ha la sua fonte.
Cosa NON fa: nessun backtest, nessuna proposta. Dice solo quali strumenti esistono e quanto
sono negoziabili oggi — il filtro che decide cosa vale la pena misurare (C6).
"""
from __future__ import annotations
import json
import sys
import time
import urllib.request
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402 (P1: l'universo si importa)
API = "https://www.deribit.com/api/v2/public/"
CACHE = ROOT / "data" / "_cache" / "r0909_deribit_universo.json"
SOGLIA_LIQUIDO_USD = 10e6 # volume 24h da cui una gamba e' "liquida" per un libro da $4-5k
def get(path: str) -> dict:
req = urllib.request.Request(API + path, headers={"User-Agent": "PythagorasGoal-research"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["result"]
def leggi(usa_cache: bool = True) -> dict:
if usa_cache and CACHE.exists():
return json.loads(CACHE.read_text())
d = dict(letto_a=datetime.now(timezone.utc).isoformat(timespec="seconds"),
fut=get("get_instruments?currency=USDC&kind=future"),
bs=get("get_book_summary_by_currency?currency=USDC&kind=future"),
opt_usdc=get("get_instruments?currency=USDC&kind=option"),
opt_inv={c: len(get(f"get_instruments?currency={c}&kind=option")) for c in ("BTC", "ETH")})
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(d))
return d
def tabella(d: dict) -> list[dict]:
perps = {i["base_currency"]: i for i in d["fut"] if i["instrument_name"].endswith("PERPETUAL") and i["state"] == "open"}
bs = {b["instrument_name"]: b for b in d["bs"]}
rows = []
for c, i in perps.items():
b = bs.get(i["instrument_name"], {})
bid, ask, mark = b.get("bid_price"), b.get("ask_price"), b.get("mark_price") or 0.0
rows.append(dict(
coin=c, listato=time.strftime("%Y-%m-%d", time.gmtime(i["creation_timestamp"] / 1000)),
min_amt=i["min_trade_amount"], min_usd=i["min_trade_amount"] * mark,
vol_usd=b.get("volume_usd") or 0.0,
spread_bps=((ask - bid) / ((ask + bid) / 2) * 1e4) if bid and ask else float("nan"),
oi_usd=(b.get("open_interest") or 0.0) * mark, funding_8h=b.get("funding_8h"),
xs01=c in XS_UNIVERSE))
return sorted(rows, key=lambda r: -r["vol_usd"])
def sintesi(rows: list[dict], d: dict) -> dict:
listati = {r["coin"] for r in rows}
liquidi = [r["coin"] for r in rows if r["vol_usd"] >= SOGLIA_LIQUIDO_USD]
bs = {b["instrument_name"]: b for b in d["bs"]}
inattivi = sorted(i["instrument_name"] for i in d["fut"] if i["instrument_name"].endswith("PERPETUAL") and i["state"] != "open")
datati = [i for i in d["fut"] if not i["instrument_name"].endswith("PERPETUAL")]
dvol_fut = []
for i in datati:
if "DVOL" not in i["instrument_name"]:
continue
b = bs.get(i["instrument_name"], {})
dvol_fut.append(dict(nome=i["instrument_name"], stato=i["state"], min_amt=i["min_trade_amount"],
vol_usd=b.get("volume_usd") or 0.0, oi=b.get("open_interest") or 0.0,
bid=b.get("bid_price"), ask=b.get("ask_price")))
return dict(
n_perp=len(rows), liquidi=liquidi, inattivi=inattivi, n_datati=len(datati), dvol_fut=dvol_fut,
xs01_listate=sorted(c for c in XS_UNIVERSE if c in listati),
xs01_mancanti=[c for c in XS_UNIVERSE if c not in listati],
opt_inv=d["opt_inv"], opt_usdc=Counter(i["base_currency"] for i in d["opt_usdc"]).most_common(),
letto_a=d["letto_a"])
def verdetto(s: dict) -> str:
return (f"{s['n_perp']} perpetual USDC; LIQUIDI (≥${SOGLIA_LIQUIDO_USD/1e6:.0f}M/g): {', '.join(s['liquidi'])}; "
f"XS01 {len(s['xs01_listate'])}/19 quotate, mancano {', '.join(s['xs01_mancanti'])}; "
f"opzioni USDC-lineari su {len(s['opt_usdc'])} sottostanti — lettura del {s['letto_a']}")
def main() -> None:
d = leggi(usa_cache="--fresh" not in sys.argv)
rows = tabella(d); s = sintesi(rows, d)
print("=" * 110)
print(f" r0909 — UNIVERSO DERIBIT (API pubblica, lettura {s['letto_a']}; --fresh per rileggere)")
print("=" * 110)
print(f"\n [1] PERPETUAL USDC-LINEARI APERTI: {s['n_perp']}")
print(f" {'coin':<9}{'listato':>11}{'min $':>8}{'vol 24h M$':>12}{'spread bps':>12}{'OI M$':>8}{'funding 8h':>12} XS01")
for r in rows:
print(f" {r['coin']:<9}{r['listato']:>11}{r['min_usd']:>8.2f}{r['vol_usd']/1e6:>12.2f}{r['spread_bps']:>12.1f}"
f"{r['oi_usd']/1e6:>8.2f}{(r['funding_8h'] or 0)*1e4:>+11.2f}bp {'<--' if r['xs01'] else ''}")
print(f" perpetual NON aperti (stato ≠ open): {s['inattivi'] or 'nessuno'}")
print(f"\n [1bis] FUTURE USDC DATATI: {s['n_datati']} — fra cui il future sul DVOL, l'unico strumento long-vol DIRETTO del venue "
f"(visto dalla revisione del 09/09: non in memoria):")
for f in s["dvol_fut"]:
spr = (f["ask"] - f["bid"]) / ((f["ask"] + f["bid"]) / 2) * 100 if f["bid"] and f["ask"] else float("nan")
print(f" {f['nome']:<28} {f['stato']:<8} min {f['min_amt']} vol24h ${f['vol_usd']/1e6:.2f}M OI {f['oi']:.0f} "
f"bid/ask {f['bid']}/{f['ask']} (spread {spr:.0f}%) → non negoziabile a questi numeri; e in contango sanguina come la put")
print(f"\n [2] XS01: {len(s['xs01_listate'])}/19 gambe quotate; mancano {s['xs01_mancanti']}")
print(f" (23/08, §50: 14/19 quotate, 11 con ≥1 anno; sulle 11 il meccanismo collassa 1,265 → 0,116 — breadth, non capitale)")
print(f"\n [3] OPZIONI: inverse {s['opt_inv']}; USDC-lineari per sottostante: {s['opt_usdc']}")
print("\n VERDETTO: " + verdetto(s))
print("=" * 110)
if __name__ == "__main__":
main()
+338
View File
@@ -0,0 +1,338 @@
"""r0909 — IL LIBRO NEI CROLLI: revisione dei sistemi adottati, misurata invece che ricordata.
DOMANDA DELL'OPERATORE (2026-09-09): "trova una soluzione per guadagnare anche nei crolli".
Prima di cercare un meccanismo nuovo, questo script misura cosa fanno OGGI i due sleeve live
(TP01 long-flat 75% + SKH01 long/short 25%) nei crolli — giorno per giorno e per episodio —
e da dove viene, se c'e', il guadagno. La memoria dice tre cose che qui vengono RIPRODOTTE
(M23: far riprodurre alla macchina il numero vecchio prima di pubblicarne uno nuovo):
(a) beta del libro al sottostante +0,076 (§46) → "non si assicura un libro gia' piatto";
(b) nel sinistro TP01 va flat e SKH01 si gira short e guadagna (§26);
(c) il peggior giorno del libro: §33 lo trova in uno short squeeze di SKH01 (−15,77% di sleeve,
2020-03-13) sulla lente HOURLY — su questa lente (canonica, sleeve giornalieri) NON si
riproduce: §33 stesso nota che la canonica da' il 2025-10-10. Qui si stampa e si dichiara.
LENTE, dichiarata: sleeve di ricerca `sleeves._tp01_returns` / `_skyhook_returns` (ancora
canonica 00:00 UTC, fee 0,10% RT, funding NON modellato, come ogni numero del progetto).
⚠️ SKH01 e' MARCATO ALL'USCITA del trade (`harness.backtest_signals` muove l'equity alla
chiusura): la serie giornaliera e' zero nell'87,7% dei giorni, e uno short aperto in una finestra
e chiuso fuori e' accreditato FUORI. E' il motivo per cui la tabella per GIORNO mostra SKH01 ≈ 0
e quella per FINESTRA mostra il guadagno. NON e' la serie del libro live (armato dal 2026-06-20): i crolli del 2026 (5 febbraio
−14%/−15%, 1-5 giugno −25%/−30%) sono PRIMA dell'arming, quindi qui sono backtest, non fill.
DEFINIZIONI, scritte prima di guardare i numeri:
- indice = 50/50 BTC/ETH, ribilanciato ogni giorno (la stessa geometria del libro);
- giorno di crollo = indice ≤ −5% (severo: ≤ −10%);
- episodio = le 12 peggiori finestre di 20 giorni NON sovrapposte dell'indice, piu' un elenco
NOMINATO di episodi noti (COVID, maggio 2021, LUNA, FTX, agosto 2024, febbraio e giugno 2026);
- classi ESCLUSIVE, in quest'ordine: "immune" = |libro| < 1/10 dell'indice (col segno stampato);
altrimenti "POSITIVO" se libro > 0, "PERDE" se < 0. Si stampa anche il conteggio nudo libro > 0.
(Prima stesura: POSITIVO provato prima di immune → 8/12; con l'ordine dichiarato qui 4/12 + 6
immune. L'ordine delle regole ERA il verdetto: corretto in revisione, si stampano entrambi.)
- beta al ribasso = pendenza OLS del libro sull'indice nei soli giorni con indice < 0.
PESI: 75/25 e' decisione vincolante (confermata 3 volte, §3). Il 37,5/62,5 di §26 compare qui
SOLO come riga informativa negli stessi episodi — non e' una proposta: la riapre solo
`weights_tilt_null`, che finora fallisce.
Nessun file di produzione toccato. Nessun ordine.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.backtest.harness import backtest_signals # noqa: E402
from src.data.downloader import load_data # noqa: E402
from src.live.book import W_SKH, W_TP01 # noqa: E402 (i pesi si IMPORTANO, non si ridichiarano: P1)
from src.portfolio.sleeves import _skyhook_returns, _tp01_returns # noqa: E402
from src.strategies.skyhook import SKH01_V2_DD, build_frames, skyhook_entries # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402
ASSETS = ("BTC", "ETH")
SOGLIA_CROLLO = -0.05
SOGLIA_SEVERO = -0.10
FINESTRA = 20
N_EPISODI = 12
BETA_MEMORIA = 0.076 # §46, da riprodurre SULLA SUA FINESTRA (DVOL: 2021-03-24+)
FINESTRA_46 = pd.Timestamp("2021-03-24", tz="UTC")
TOL_BETA = 0.01
W_INFO = (0.375, 0.625) # §26: riga informativa, NON proposta
EPISODI_NOMINATI = {
"COVID 2020-03": ("2020-03-01", "2020-03-20"),
"maggio 2021": ("2021-05-08", "2021-05-24"),
"LUNA 2022-05": ("2022-05-04", "2022-05-14"),
"giugno 2022": ("2022-06-06", "2022-06-19"),
"FTX 2022-11": ("2022-11-06", "2022-11-22"),
"agosto 2024": ("2024-07-28", "2024-08-06"),
"febbraio 2025": ("2025-01-31", "2025-03-11"),
"5 febbraio 2026": ("2026-01-28", "2026-02-07"),
"1-5 giugno 2026": ("2026-05-28", "2026-06-06"),
}
# ------------------------------------------------------------------ serie
def indice_5050() -> pd.Series:
r = {}
for a in ASSETS:
d = resample_1d(load_data(a, "1h"))
s = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["datetime"], utc=True))
r[a] = s.pct_change()
J = pd.concat(r, axis=1, join="inner").dropna()
return 0.5 * J["BTC"] + 0.5 * J["ETH"]
def tp01_esposizione() -> pd.Series:
"""Esposizione lorda media (BTC+ETH)/2 tenuta nel giorno (decisa a close[i-1])."""
tp = TrendPortfolio(**CANONICAL)
e = {}
for a in ASSETS:
d = resample_1d(load_data(a, "1h"))
tgt = tp.target_series(d)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
e[a] = pd.Series(held, index=pd.to_datetime(d["datetime"], utc=True))
J = pd.concat(e, axis=1, join="inner")
return 0.5 * J["BTC"] + 0.5 * J["ETH"]
def skh_per_direzione() -> dict[str, pd.Series]:
"""SKH01-V2-DD 50/50 su griglia giornaliera, in tre versioni: intera, SOLO long, SOLO short.
Le versioni per direzione filtrano gli entry-dict prima del backtest: stesso motore, stessi
stop. In principio filtrare potrebbe cambiare il non-overlap; MISURATO (revisione 09/09):
long + short == intera su 2735/2735 giorni (max|diff| 3,5e-18) — la scomposizione e' ESATTA,
e `misura()` lo verifica con un assert (P1: la versione "tutto" e' confrontata bit-exact con
`sleeves._skyhook_returns`).
"""
out = {k: {} for k in ("tutto", "long", "short")}
for a in ASSETS:
ltf, htf = build_frames(load_data(a, "5m"))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
for k, keep in (("tutto", (1, -1)), ("long", (1,)), ("short", (-1,))):
e2 = [x if (x is not None and x["dir"] in keep) else None for x in ent]
m = backtest_signals(ltf, e2, fee_rt=0.001, leverage=1.0, asset=a, tf="230m")
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
out[k][a] = s.resample("1D").last().ffill().pct_change().dropna()
res = {}
for k in out:
J = pd.concat(out[k], axis=1, join="inner").fillna(0.0)
res[k] = 0.5 * J["BTC"] + 0.5 * J["ETH"]
return res
# ------------------------------------------------------------------ misure
def sh(s: pd.Series) -> float:
s = s.dropna()
return float(s.mean() / s.std() * np.sqrt(365.0)) if len(s) > 5 and s.std() > 0 else float("nan")
def maxdd(s: pd.Series) -> float:
eq = (1 + s.fillna(0)).cumprod(); return float((eq / eq.cummax() - 1).min())
def cum(s: pd.Series) -> float:
return float((1 + s.fillna(0)).prod() - 1)
def finestre_peggiori(idx: pd.Series, n: int = N_EPISODI, w: int = FINESTRA) -> list[tuple]:
"""Le n peggiori finestre di w giorni NON sovrapposte dell'indice (greedy dal peggiore)."""
roll = (1 + idx).rolling(w).apply(np.prod, raw=True) - 1
roll = roll.dropna().sort_values()
prese, occupati = [], []
for end, v in roll.items():
start = end - pd.Timedelta(days=w - 1)
if any(not (end < s or start > e) for s, e in occupati):
continue
prese.append((start, end, float(v))); occupati.append((start, end))
if len(prese) >= n:
break
return sorted(prese)
def classifica(libro: float, indice: float) -> str:
"""Classi esclusive nell'ordine dichiarato: immune (col segno) → POSITIVO → PERDE."""
if abs(libro) < abs(indice) / 10:
return "immune+" if libro > 0 else "immune-"
return "POSITIVO" if libro > 0 else "PERDE"
def beta_ribasso(libro: pd.Series, idx: pd.Series, soglia: float = 0.0) -> float:
J = pd.concat({"b": libro, "i": idx}, axis=1, join="inner").dropna()
J = J[J["i"] < soglia]
if len(J) < 20:
return float("nan")
x, y = J["i"].values, J["b"].values
return float(np.cov(x, y)[0, 1] / np.var(x, ddof=1))
def beta_pieno(libro: pd.Series, idx: pd.Series) -> float:
J = pd.concat({"b": libro, "i": idx}, axis=1, join="inner").dropna()
x, y = J["i"].values, J["b"].values
return float(np.cov(x, y)[0, 1] / np.var(x, ddof=1))
def misura() -> dict:
idx = indice_5050()
tp = _tp01_returns()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
skh = skh_per_direzione()
esp = tp01_esposizione()
J = pd.concat({"idx": idx, "tp": tp, "skh": skh["tutto"], "skl": skh["long"],
"sks": skh["short"], "esp": esp}, axis=1, join="inner").dropna()
ref = _skyhook_returns()
if ref.index.tz is None:
ref.index = ref.index.tz_localize("UTC")
comune = J.index.intersection(ref.index)
assert float((J.loc[comune, "skh"] - ref.loc[comune]).abs().max()) == 0.0, "SKH01 'tutto' non e' bit-exact col sleeve"
assert float((J["skl"] + J["sks"] - J["skh"]).abs().max()) < 1e-12, "long + short != intera: la scomposizione non e' esatta"
J["libro"] = W_TP01 * J["tp"] + W_SKH * J["skh"]
J["info"] = W_INFO[0] * J["tp"] + W_INFO[1] * J["skh"]
J["c_tp"] = W_TP01 * J["tp"]
J["c_skh"] = W_SKH * J["skh"]
J["c_sks"] = W_SKH * J["sks"]
# giorni di crollo
giorni = {}
for nome, soglia in (("≤ −5%", SOGLIA_CROLLO), ("≤ −10%", SOGLIA_SEVERO)):
g = J[J["idx"] <= soglia]
giorni[nome] = dict(n=int(len(g)), idx=float(g["idx"].mean()), libro=float(g["libro"].mean()),
tp=float(g["c_tp"].mean()), skh=float(g["c_skh"].mean()),
sks=float(g["c_sks"].mean()), esp=float(g["esp"].mean()),
libro_pos=float((g["libro"] > 0).mean()),
per_anno={int(y): dict(n=int(len(gg)), idx=float(gg["idx"].sum()),
libro=float(gg["libro"].sum()),
sks=float(gg["c_sks"].sum()))
for y, gg in g.groupby(g.index.year)})
# episodi: finestre peggiori + nominati
def riga_ep(nome, s, e):
w = J.loc[s:e]
if len(w) < 3:
return None
return dict(nome=nome, da=str(w.index[0].date()), a=str(w.index[-1].date()), n=int(len(w)),
idx=cum(w["idx"]), libro=cum(w["libro"]), tp=cum(w["c_tp"]), skh=cum(w["c_skh"]),
sks=cum(w["c_sks"]), skl=cum(W_SKH * w["skl"]), esp=float(w["esp"].mean()),
info=cum(w["info"]), classe=classifica(cum(w["libro"]), cum(w["idx"])))
ep_auto = [r for r in (riga_ep(f"peggiore #{i+1}", s, e)
for i, (s, e, _) in enumerate(finestre_peggiori(J["idx"]))) if r]
ep_nom = [r for r in (riga_ep(k, pd.Timestamp(a, tz="UTC"), pd.Timestamp(b, tz="UTC"))
for k, (a, b) in EPISODI_NOMINATI.items()) if r]
# peggior giorno del libro (riproduce §33: e' uno squeeze, non un crollo?)
peggiori = J.nsmallest(5, "libro")[["idx", "libro", "c_tp", "c_skh"]]
n_auto = len(ep_auto)
guad = sum(r["classe"] == "POSITIVO" for r in ep_auto)
imm = sum(r["classe"].startswith("immune") for r in ep_auto)
pos = sum(r["libro"] > 0 for r in ep_auto)
dal22 = [r for r in ep_auto if r["da"] >= "2022"]
J46 = J[J.index >= FINESTRA_46]
quota_short = (sum(r["sks"] for r in ep_auto if r["libro"] > 0)
/ max(1e-12, sum(r["libro"] for r in ep_auto if r["libro"] > 0)))
return dict(
finestra=(str(J.index[0].date()), str(J.index[-1].date()), int(len(J))),
beta=beta_pieno(J["libro"], J["idx"]), beta_46=beta_pieno(J46["libro"], J46["idx"]),
beta_giu=beta_ribasso(J["libro"], J["idx"]),
beta_giu3=beta_ribasso(J["libro"], J["idx"], -0.03),
sharpe=dict(libro=sh(J["libro"]), info=sh(J["info"]), tp=sh(J["tp"]), skh=sh(J["skh"]),
sks=sh(J["sks"]), skl=sh(J["skl"])),
maxdd=dict(libro=maxdd(J["libro"]), info=maxdd(J["info"]), idx=maxdd(J["idx"])),
giorni=giorni, episodi=ep_auto, nominati=ep_nom,
peggiori=[(str(t.date()), float(r.idx), float(r.libro), float(r.c_tp), float(r.c_skh))
for t, r in peggiori.iterrows()],
sks_anno={int(y): cum(g) for y, g in J["c_sks"].groupby(J.index.year)},
sintesi=dict(n_episodi=n_auto, guadagna=guad, immune=imm, perde=n_auto - guad - imm, positivi=pos,
dal22=(sum(r["libro"] > 0 for r in dal22), len(dal22)),
giorni_pos=giorni["≤ −5%"]["libro_pos"], giorni_libro=giorni["≤ −5%"]["libro"],
quota_short_nei_guadagni=float(quota_short),
info_batte_live=sum(r["info"] > r["libro"] for r in ep_auto)),
)
def verdetto(m: dict) -> str:
"""Tre affermazioni separate, perche' rispondono a tre domande diverse:
(1) nel GIORNO del crollo (media sui giorni ≤ −5%); (2) per FINESTRA (classi esclusive);
(3) da dove viene il guadagno. Un solo aggettivo ("guadagna"/"immune") sarebbe l'ordine delle regole."""
s = m["sintesi"]
beta_ok = abs(m["beta_46"] - BETA_MEMORIA) < TOL_BETA
giorno = ("PERDE" if s["giorni_libro"] < 0 else "GUADAGNA")
if s["guadagna"] >= 2 * s["n_episodi"] / 3:
finestra = "POSITIVO per finestra"
elif s["guadagna"] + s["immune"] >= 2 * s["n_episodi"] / 3:
finestra = "IMMUNE-O-POSITIVO per finestra"
else:
finestra = "PERDE per finestra"
return (f"NEL GIORNO DEL CROLLO IL LIBRO {giorno} ({s['giorni_libro']*100:+.2f}%/g, positivo nel {s['giorni_pos']*100:.0f}% dei giorni ≤ −5%); "
f"{finestra}: {s['guadagna']} POSITIVO / {s['immune']} immune / {s['perde']} PERDE su {s['n_episodi']} (libro > 0 in {s['positivi']}, "
f"dal 2022 in {s['dal22'][0]}/{s['dal22'][1]}); beta {m['beta_46']:+.4f} sulla finestra di §46 "
f"({'riproduce' if beta_ok else 'NON riproduce'} il +{BETA_MEMORIA:.3f}); "
f"la gamba short di SKH01 (marcata all'uscita) fa il {s['quota_short_nei_guadagni']*100:.0f}% dei guadagni negli episodi con libro > 0")
# ------------------------------------------------------------------ stampa
def pct(x: float) -> str:
return f"{x*100:+.2f}%"
def main() -> None:
print("=" * 100)
print(" r0909 — IL LIBRO NEI CROLLI (TP01 75 / SKH01 25, lente di ricerca, ancora canonica)")
print("=" * 100)
m = misura()
a, b, n = m["finestra"]
print(f" finestra {a} → {b} ({n} giorni) pesi importati da src/live/book: TP01 {W_TP01} / SKH01 {W_SKH}")
print("\n [1] BETA DEL LIBRO ALL'INDICE 50/50")
print(f" pieno {m['beta']:+.3f} · sulla finestra di §46 (2021-03-24+) {m['beta_46']:+.4f} (memoria: +{BETA_MEMORIA:.3f}) "
f"al ribasso (idx<0) {m['beta_giu']:+.3f} nei giorni ≤ −3% {m['beta_giu3']:+.3f}")
print("\n [2] GIORNI DI CROLLO — media per giorno")
print(f" {'soglia':<8}{'n':>5}{'indice':>9}{'libro':>9}{'TP01':>9}{'SKH01':>9}{'SKH short':>11}{'esp TP01':>10}{'libro>0':>9}")
for k, g in m["giorni"].items():
print(f" {k:<8}{g['n']:>5}{pct(g['idx']):>9}{pct(g['libro']):>9}{pct(g['tp']):>9}"
f"{pct(g['skh']):>9}{pct(g['sks']):>11}{g['esp']:>10.3f}{g['libro_pos']*100:>8.0f}%")
print("\n per anno (giorni ≤ −5%, SOMMA nel gruppo):")
print(f" {'anno':<6}{'n':>4}{'indice':>10}{'libro':>10}{'SKH short':>11}")
for y, g in m["giorni"]["≤ −5%"]["per_anno"].items():
print(f" {y:<6}{g['n']:>4}{pct(g['idx']):>10}{pct(g['libro']):>10}{pct(g['sks']):>11}")
def tab(rows, titolo):
print(f"\n {titolo}")
print(f" {'episodio':<18}{'da':>11}{'a':>11}{'indice':>9}{'libro':>9}{'TP01':>8}{'SKH01':>8}"
f"{'SKH-S':>8}{'SKH-L':>8}{'esp':>6}{'37/63':>8} classe")
for r in rows:
print(f" {r['nome']:<18}{r['da']:>11}{r['a']:>11}{pct(r['idx']):>9}{pct(r['libro']):>9}"
f"{pct(r['tp']):>8}{pct(r['skh']):>8}{pct(r['sks']):>8}{pct(r['skl']):>8}{r['esp']:>6.2f}"
f"{pct(r['info']):>8} {r['classe']}")
tab(m["episodi"], f"[3] LE {N_EPISODI} PEGGIORI FINESTRE DI {FINESTRA} GIORNI DELL'INDICE (non sovrapposte)")
tab(m["nominati"], "[4] EPISODI NOMINATI (elenco scritto prima di misurare)")
print("\n [5] I 5 PEGGIORI GIORNI DEL LIBRO (§33 su lente hourly: squeeze SKH01 2020-03-13; qui lente canonica: NON si riproduce, e' un crollo con TP01 mezzo long)")
print(f" {'giorno':<12}{'indice':>9}{'libro':>9}{'TP01':>9}{'SKH01':>9}")
for t, i, l, tpv, sk in m["peggiori"]:
print(f" {t:<12}{pct(i):>9}{pct(l):>9}{pct(tpv):>9}{pct(sk):>9}")
print("\n [6] LA GAMBA SHORT DI SKH01 (peso 0,25 gia' applicato) — contributo per anno al libro")
print(" " + " ".join(f"{y} {pct(v)}" for y, v in m["sks_anno"].items()))
s = m["sharpe"]; d = m["maxdd"]
print(f"\n Sharpe: libro {s['libro']:.2f} · TP01 {s['tp']:.2f} · SKH01 {s['skh']:.2f} "
f"(solo long {s['skl']:.2f} / solo short {s['sks']:.2f}) maxDD libro {d['libro']*100:.1f}% · indice {d['idx']*100:.1f}%")
print(f" riga INFORMATIVA 37,5/62,5 (§26, NON proposta): Sharpe {s['info']:.2f}, maxDD {d['info']*100:.1f}%, "
f"batte il 75/25 in {m['sintesi']['info_batte_live']}/{m['sintesi']['n_episodi']} episodi")
print("\n VERDETTO: " + verdetto(m))
print("=" * 100)
if __name__ == "__main__":
main()
+242
View File
@@ -0,0 +1,242 @@
"""r0909 — XRP COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI): la misura che manca.
PERCHE' ESISTE. L'operatore chiede "piu' monete, ma sempre in Deribit". Letto il venue OGGI
(09/09/2026, `public/get_book_summary_by_currency`): dei 32 perpetual USDC-lineari, solo
QUATTRO hanno volume a 8 cifre — BTC, ETH, **XRP ($38M/g, il PIU' scambiato del venue, spread
4 bps, listato 2022-03-16)** e SOL ($13M/g). Tutto il resto sta fra $0,01M e $7M al giorno.
SOL come terza gamba e' stato misurato il 22/08 e ESCLUSO dall'operatore (hold-out −0,166 in
0/24 ancore, guadagno di UN anno, il 2023). XRP non e' mai stato misurato come gamba
direzionale: e' l'unico candidato "piu' monete" del venue che sia liquido E non misurato.
IPOTESI A PRIORI, dichiarata prima di misurare: DILUISCE, come SOL — il trend multi-asset del
19/06 fu scartato a corr 0,74 con TP01, e SOL ha confermato (corr 0,40-0,56, in salita col dato
che migliora). Se XRP aggiunge, il primo sospetto e' l'anno del suo evento idiosincratico (la
sentenza SEC dell'estate 2023 e la corsa di fine 2024): M9, "24/24 ancore positive possono
essere un anno solo".
METODO — identico a `r0822_sol_leg.py`, da cui IMPORTA il harness (non riscritto): TP01 e
SKH01-V2-DD congelati, book a 2 vs 3 gambe, 24 ancore orarie, mediana delle differenze
APPAIATE (M7), null del de-levering a iso-maxDD (M5), scomposizione per anno (M9), due lenti
DICHIARATE PRIMA dalla certificazione del dato, non dal risultato:
- L-FULL: dal listing (2022-03-16);
- L-PULITA: dal primo anno in cui la quota di barre 1h a >1% da Coinbase USD scende sotto
lo 0,5% (SOL 2023 aveva 0,6% e fu giudicata sporca: e' il precedente che fissa la soglia).
Il dato XRP vive in `data/raw/alt_xrp_*.parquet` (namespace di ricerca, come SOL: fuori dal
feed attivo, NON rinfrescato dal cron, `load_data("XRP")` continua a fallire).
Nessun file di produzione toccato. Nessun ordine. Book/pesi/universo/cron INVARIATI.
"""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from r0822_sol_leg import ( # noqa: E402 — harness importato, non riscritto
DUE, HOLDOUT, W_SKH, W_TP, book, cagr, dati, k_iso_dd, maxdd, sh, skh_leg, taglia, tp01_leg,
)
ASSET = "XRP"
TRE = ("BTC", "ETH", ASSET)
LISTING = pd.Timestamp("2022-03-16", tz="UTC")
SOGLIA_PULITA = 0.005 # quota di barre 1h a >1% da Coinbase: sotto = anno pulito
CACHE = ROOT / "data" / "_cache" / "r0909_xrp_riferimento_1h.parquet"
# ------------------------------------------------------------------ certificazione (D1: Coinbase = audit)
def _fetch_1h(ex, symbol: str, start: pd.Timestamp, limit: int) -> pd.Series:
"""OHLCV 1h paginato in avanti; un batch vuoto NON ferma il ciclo (Coinbase ha un buco
2021-01 → 2023-07 su XRP: delistato dopo la causa SEC, rilistato dopo la sentenza)."""
out, since = [], int(start.timestamp() * 1000)
end = int(time.time() * 1000)
while since < end:
r = ex.fetch_ohlcv(symbol, "1h", since=since, limit=limit)
r = [x for x in r if x[0] >= since]
if not r:
since += limit * 3_600_000
continue
out.extend(r)
nxt = r[-1][0] + 3_600_000
since = nxt if nxt > since else since + limit * 3_600_000
df = pd.DataFrame(out, columns=["ts", "o", "h", "l", "close", "v"]).drop_duplicates("ts")
return pd.Series(df["close"].values.astype(float),
index=pd.to_datetime(df["ts"], unit="ms", utc=True)).sort_index()
def riferimento_1h(start: pd.Timestamp) -> pd.DataFrame:
"""Riferimento indipendente in USD (D1: audit, mai ancora): Coinbase dove c'e' (dal rilisting
2023-07), Bitstamp altrove — entrambi gia' usati dal progetto come venue di audit. Con cache."""
if CACHE.exists():
df = pd.read_parquet(CACHE); df.index = pd.to_datetime(df.index, utc=True)
if len(df):
return df
import ccxt
bs = _fetch_1h(ccxt.bitstamp({"enableRateLimit": True}), "XRP/USD", start, 1000)
cb = _fetch_1h(ccxt.coinbase({"enableRateLimit": True}), "XRP/USD", start, 300)
df = pd.concat({"bitstamp": bs, "coinbase": cb}, axis=1).sort_index()
df["close"] = df["coinbase"].where(df["coinbase"].notna(), df["bitstamp"])
df["fonte"] = np.where(df["coinbase"].notna(), "CB", np.where(df["bitstamp"].notna(), "BS", ""))
CACHE.parent.mkdir(parents=True, exist_ok=True)
df.to_parquet(CACHE)
return df
def certifica() -> dict:
"""Per anno: quota di barre 1h a >1% da Coinbase, mediana in bps, quota di barre flat (1h e 5m)."""
d1 = dati(ASSET, "1h")
dz = pd.Series(d1["close"].values.astype(float), index=pd.to_datetime(d1["datetime"], utc=True))
ref = riferimento_1h(LISTING)
J = pd.concat({"d": dz, "c": ref["close"], "f": ref["fonte"]}, axis=1, join="inner").dropna(subset=["d", "c"])
dev = (J["d"] - J["c"]).abs() / J["c"]
d5 = dati(ASSET, "5m")
f5 = pd.Series((d5["high"].values == d5["low"].values), index=pd.to_datetime(d5["datetime"], utc=True))
f1 = pd.Series((d1["high"].values == d1["low"].values), index=dz.index)
per_anno = {}
for y in sorted(set(J.index.year)):
m = dev[dev.index.year == y]
fy = J["f"][J.index.year == y]
per_anno[int(y)] = dict(n=int(len(m)), q_1pct=float((m > 0.01).mean()), med_bps=float(m.median() * 1e4),
fonte=f"CB {float((fy == 'CB').mean())*100:.0f}% / BS {float((fy == 'BS').mean())*100:.0f}%",
flat_1h=float(f1[f1.index.year == y].mean()), flat_5m=float(f5[f5.index.year == y].mean()))
puliti = [y for y, r in per_anno.items() if r["q_1pct"] <= SOGLIA_PULITA]
# L-PULITA = dal primo anno pulito in poi SOLO se anche i successivi restano puliti (un anno pulito
# isolato non e' un regime); altrimenti dal primo della coda pulita finale.
anni = sorted(per_anno)
inizio = None
for y in anni:
if all(per_anno[z]["q_1pct"] <= SOGLIA_PULITA for z in anni if z >= y):
inizio = y; break
return dict(per_anno=per_anno, puliti=puliti,
pulita_da=pd.Timestamp(f"{inizio}-01-01", tz="UTC") if inizio else None)
# ------------------------------------------------------------------ misura (stesso schema di r0822_sol_leg)
def gamba(asset: str) -> pd.Series:
return W_TP * tp01_leg(asset, 0).reindex(skh_leg(asset).index).fillna(0.0) + W_SKH * skh_leg(asset)
def differenze(anchors, start) -> dict:
d = {"sh": [], "hold": [], "dd": [], "cagr": []}
for h in anchors:
A, B = book(DUE, h), book(TRE, h)
idx = A.index.intersection(B.index)
A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start)
if len(A) < 200:
continue
d["sh"].append(sh(B) - sh(A)); d["hold"].append(sh(taglia(B, HOLDOUT)) - sh(taglia(A, HOLDOUT)))
d["dd"].append((maxdd(B) - maxdd(A)) * 100); d["cagr"].append((cagr(B) - cagr(A)) * 100)
return {k: np.array(v, float) for k, v in d.items()}
def misura(n_anc: int = 24) -> dict:
anchors = list(range(0, 24, max(1, 24 // n_anc)))[:n_anc]
cert = certifica()
lenti = {"L-FULL": None}
if cert["pulita_da"] is not None:
lenti[f"L-PULITA ({cert['pulita_da'].year}+)"] = cert["pulita_da"]
lenti["2024+ (lente di SOL)"] = pd.Timestamp("2024-01-01", tz="UTC")
out = dict(cert=cert, lenti={}, gambe={}, anno={}, delev={}, corr={})
for a in TRE:
out["gambe"][f"TP01 {a}"] = tp01_leg(a, 0); out["gambe"][f"SKH01 {a}"] = skh_leg(a)
g = gamba(ASSET); b2 = book(DUE, 0)
com = g.index.intersection(b2.index)
for nome, start in lenti.items():
c = com if start is None else com[com >= start]
out["corr"][nome] = float(np.corrcoef(g.loc[c], b2.loc[c])[0, 1]) if len(c) > 100 else np.nan
out["lenti"][nome] = differenze(anchors, start)
A, B = book(DUE, 0), book(TRE, 0); idx = A.index.intersection(B.index)
A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start)
k = k_iso_dd(A, B)
out["delev"][nome] = dict(k=k, sh3=sh(B), sh2k=sh(k * A), cagr3=cagr(B), cagr2k=cagr(k * A))
A0, B0 = book(DUE, 0), book(TRE, 0); i0 = A0.index.intersection(B0.index); A0, B0 = A0.loc[i0], B0.loc[i0]
for y in sorted(set(A0.index.year)):
a_, b_ = A0[A0.index.year == y], B0[B0.index.year == y]
if len(a_) >= 60:
out["anno"][int(y)] = dict(sh2=sh(a_), sh3=sh(b_), dsh=sh(b_) - sh(a_), dcagr=(cagr(b_) - cagr(a_)) * 100,
sh_gamba=sh(g[g.index.year == y]))
out["gamba_xrp"] = g
out["n_anchors"] = len(anchors)
return out
def verdetto(m: dict) -> str:
"""AGGIUNGE solo se, nella lente PULITA (o 2024+ se non c'e' una coda pulita): dSharpe hold-out
mediano > 0 in ≥ 75% delle ancore, E il null del de-levering passa (Sh 3 gambe > Sh 2 gambe
riscalate + 0,02), E il contributo per anno e' positivo in piu' anni di quanti sia negativo —
contati sugli anni DENTRO la lente (derivati dal suo nome, non cablati: P1).
Un campione vuoto e' "NON MISURABILE", non un verdetto (P5: "non vedo" non e' "va tutto bene")."""
nomi = [n for n in m["lenti"] if n.startswith("L-PULITA")] or ["2024+ (lente di SOL)"]
n = nomi[0]; d = m["lenti"][n]; dl = m["delev"][n]
import re
anno0 = int(re.search(r"(\d{4})\+", n).group(1)) # "L-PULITA (2024+)" / "2024+ (lente di SOL)"
if not len(d["hold"]) or not np.isfinite(dl["sh3"]) or not np.isfinite(dl["sh2k"]):
return f"XRP: NON MISURABILE — lente {n} senza ancore valide o de-levering non calcolabile"
c_hold = np.median(d["hold"]) > 0 and np.mean(d["hold"] > 0) >= 0.75
c_delev = dl["sh3"] > dl["sh2k"] + 0.02
anni = [r["dsh"] for y, r in m["anno"].items() if y >= anno0]
c_anni = sum(x > 0 for x in anni) > sum(x < 0 for x in anni)
esito = "XRP AGGIUNGE" if (c_hold and c_delev and c_anni) else ("XRP DILUISCE" if (not c_hold and not c_delev) else "XRP: PARI / NON SELEZIONABILE")
return (f"{esito} — lente {n}: dSharpe hold-out mediano {np.median(d['hold']):+.3f} "
f"(>0 nel {np.mean(d['hold'] > 0) * 100:.0f}% di {len(d['hold'])} ancore); "
f"de-levering: Sh 3 gambe {dl['sh3']:.2f} vs 2 gambe×{dl['k']:.3f} {dl['sh2k']:.2f}; "
f"anni {anno0}+ con dSh>0: {sum(x > 0 for x in anni)}/{len(anni)}")
def main() -> None:
n_anc = int(sys.argv[sys.argv.index("--anchors") + 1]) if "--anchors" in sys.argv else 24
print("=" * 104)
print(" r0909 — XRP COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI; harness di r0822_sol_leg)")
print("=" * 104)
print(" ipotesi a priori dichiarata: DILUISCE (come SOL; trend multi-asset 19/06 corr 0,74)")
m = misura(n_anc)
c = m["cert"]
print(f"\n [0/5] CERTIFICAZIONE XRP vs Coinbase USD (1h) — soglia anno pulito: quota >1% ≤ {SOGLIA_PULITA*100:.1f}%")
print(f" {'anno':<6}{'barre':>7}{'>1%':>8}{'med bps':>9}{'flat 1h':>9}{'flat 5m':>9} riferimento")
for y, r in c["per_anno"].items():
print(f" {y:<6}{r['n']:>7}{r['q_1pct']*100:>7.2f}%{r['med_bps']:>9.1f}{r['flat_1h']*100:>8.1f}%{r['flat_5m']*100:>8.1f}% {r['fonte']}")
print(f" L-PULITA da: {c['pulita_da'].date() if c['pulita_da'] is not None else 'NESSUN anno pulito in coda'}")
print(f"\n [1/5] LE GAMBE DA SOLE (ancora canonica h=0)")
print(f"\n {'gamba':<26}{'barre':>7}{'Sharpe':>9}{'maxDD':>10}{'CAGR':>9}{'Sh hold':>10}")
for k, s in m["gambe"].items():
print(f" {k:<26}{len(s):>7}{sh(s):>9.2f}{maxdd(s)*100:>9.1f}%{cagr(s)*100:>8.1f}%{sh(taglia(s, HOLDOUT)):>10.2f}")
g = m["gamba_xrp"]
print(f"\n [2/5] LA GAMBA XRP (75/25) — Sharpe {sh(g):.2f}, maxDD {maxdd(g)*100:.1f}%, CAGR {cagr(g)*100:.1f}%, hold-out {sh(taglia(g, HOLDOUT)):.2f}")
for n, v in m["corr"].items():
print(f" corr(gamba XRP, book BTC/ETH) {n}: {v:+.3f} (SOL: +0,40 / +0,56; soglia del 19/06: 0,74)")
print(f"\n [3/5] BOOK A 2 vs 3 GAMBE — differenze APPAIATE per ancora ({m['n_anchors']} ancore)")
for n, d in m["lenti"].items():
if not len(d["sh"]):
print(f"\n {n}: campione insufficiente"); continue
print(f"\n {n} ({len(d['sh'])} ancore)")
for lab, v, u in (("dSharpe FULL", d["sh"], ""), ("dSharpe hold-out", d["hold"], ""), ("dMaxDD", d["dd"], " pp"), ("dCAGR", d["cagr"], " pp")):
print(f" {lab:<18} mediana {np.median(v):+7.3f}{u} banda [{np.percentile(v, 10):+.3f}, {np.percentile(v, 90):+.3f}] >0 nel {np.mean(v > 0)*100:.0f}% delle ancore")
print(f"\n [3bis] PER ANNO (h=0): {'anno':<6}{'2g Sh':>7}{'3g Sh':>7}{'dSh':>7}{'dCAGR':>9}{'gamba XRP':>11}")
for y, r in m["anno"].items():
print(f" {'':<19}{y:<6}{r['sh2']:>7.2f}{r['sh3']:>7.2f}{r['dsh']:>+7.2f}{r['dcagr']:>+8.1f}pp{r['sh_gamba']:>+11.2f}")
print(f"\n [4/5] NULL DEL DE-LEVERING (iso-maxDD): {'lente':<24}{'k':>7}{'Sh 3g':>8}{'Sh 2g×k':>9}{'CAGR 3g':>9}{'CAGR 2g×k':>11}")
for n, dl in m["delev"].items():
v = "AGGIUNGE" if dl["sh3"] > dl["sh2k"] + 0.02 else ("de-levering" if dl["sh2k"] > dl["sh3"] + 0.02 else "pari")
print(f" {'':<39}{n:<24}{dl['k']:>7.3f}{dl['sh3']:>8.2f}{dl['sh2k']:>9.2f}{dl['cagr3']*100:>8.1f}%{dl['cagr2k']*100:>10.1f}% {v}")
px = float(dati(ASSET, "1h")["close"].iloc[-1])
print(f"\n [5/5] ESEGUIBILITA': XRP_USDC-PERPETUAL min 1 XRP = ${px:.2f} < pavimento min_order $5 — non e' il vincolo (come SOL)")
print("\n VERDETTO: " + verdetto(m))
print("=" * 104)
if __name__ == "__main__":
main()
+298
View File
@@ -0,0 +1,298 @@
#!/usr/bin/env python
"""r0910_macro_riskoff.py — «risk-off» di TP01 nella finestra ±2h intorno a FOMC e CPI: vale qualcosa?
DOMANDA (operatore, 2026-09-10, da un articolo su EA forex con «agente fondamentale»): se TP01 si
mettesse FLAT nelle due ore prima e dopo la dichiarazione FOMC (14:00 ET) e la release CPI
(08:30 ET), guadagnerebbe Sharpe o drift? E' un risk-off, quindi per la regola di §3 («non
de-esporre il weekend») PARTE REFUTED e deve superare il null del de-levering (M5) e un null
location-matched (M18) prima di essere creduto.
VERDETTO PRE-REGISTRATO (scritto PRIMA di vedere un numero):
LEAD solo se TUTTE: (a) ΔSharpe(riskoff − base) > 0 con le fee di chiusura/riapertura;
(b) il Δ vero sta al ≥95° percentile del null location-matched (stesso numero di eventi per
anno, stesse ore del giorno, giorni estratti a caso); (c) Δ positivo in ≥70% degli anni con ≥5
eventi (M9). Altrimenti REFUTED. E l'effetto si cita anche in €/giorno all'equity di oggi.
COSA NON C'E' (dichiarato): il funding (mai in nessun backtest, −2,16%/anno di drift) — irrilevante
qui perche' identico nelle due varianti salvo ~1% delle ore; il rimbalzo di prezzo nella finestra
(slippage: TP01 chiude e riapre a mid, in un'ora in cui lo spread e' largo — se il risultato fosse
positivo andrebbe rimisurato col costo vero della finestra, C1). SKH01 non e' qui: e' un breakout su
barra 230m, appiattirlo cambierebbe il percorso del segnale.
CALENDARIO (letto dal web il 2026-09-10, non ricordato — regola globale):
FOMC: federalreserve.gov/monetarypolicy/fomccalendars.htm (2021-2026) e fomchistorical2019/2020.
Ore: 14:00 ET per le riunioni programmate (standard Fed); le due d'emergenza del 2020
(03/03 10:00 ET, 15/03 17:00 ET) con la loro ora; i «notation vote» ESCLUSI (nessuna
dichiarazione a orario). ET→UTC con America/New_York (DST).
CPI: bls.gov/bls/news-release/cpi.htm (archivio 2019-2026), 08:30 ET (bls.gov/schedule).
L'archivio raggruppa per anno di RIFERIMENTO: le release di gennaio/febbraio stanno sotto
l'anno prima — corrette qui (+1 anno), e la correzione e' verificata sul 2026 (13/01 e
13/02 stanno nella schedule ufficiale 2026).
TP01 ORARIO: posizione decisa alla chiusura giornaliera (00:00 UTC) e tenuta per le 24 barre 1h del
giorno (la stessa `held` di sleeves._tp01_returns, spalmata sulle ore); P&L orario = held × rendimento
1h; fee sul turnover giornaliero. RISK-OFF: held = 0 nelle barre 1h che intersecano [t−2h, t+2h],
con fee_side su chiusura e riapertura (|held| × 2 × fee).
uv run python scripts/research/r0910_macro_riskoff.py
"""
from __future__ import annotations
import sys
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.data.downloader import load_data # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d # noqa: E402
ASSETS = ("BTC", "ETH")
ET = ZoneInfo("America/New_York")
FINESTRA_H = 2.0
N_NULL = 1000
SEED = 20260910
EQUITY_OGGI = 4454.83 # trades_db --report 10/09 14:21Z, per il €/giorno
EURUSD = 1.08 # ordine di grandezza, dichiarato: serve solo per la riga in €/g
# ------------------------------------------------------------------ calendario
FOMC_PROGRAMMATE = """
2019-01-30 2019-03-20 2019-05-01 2019-06-19 2019-07-31 2019-09-18 2019-10-30 2019-12-11
2020-01-29 2020-04-29 2020-06-10 2020-07-29 2020-09-16 2020-11-05 2020-12-16
2021-01-27 2021-03-17 2021-04-28 2021-06-16 2021-07-28 2021-09-22 2021-11-03 2021-12-15
2022-01-26 2022-03-16 2022-05-04 2022-06-15 2022-07-27 2022-09-21 2022-11-02 2022-12-14
2023-02-01 2023-03-22 2023-05-03 2023-06-14 2023-07-26 2023-09-20 2023-11-01 2023-12-13
2024-01-31 2024-03-20 2024-05-01 2024-06-12 2024-07-31 2024-09-18 2024-11-07 2024-12-18
2025-01-29 2025-03-19 2025-05-07 2025-06-18 2025-07-30 2025-09-17 2025-10-29 2025-12-10
2026-01-28 2026-03-18 2026-04-29 2026-06-17 2026-07-29
""".split()
FOMC_EMERGENZA = [("2020-03-03", "10:00"), ("2020-03-15", "17:00")]
# archivio BLS, ordine della pagina (per anno di riferimento): le release di gen/feb vanno +1 anno
CPI_ARCHIVIO = """
2026-08-12 2026-07-14 2026-06-10 2026-05-12 2026-04-10 2026-03-11 2026-02-13
2025-01-13 2025-12-18 2025-10-24 2025-09-11 2025-08-12 2025-07-15 2025-06-11 2025-05-13 2025-04-10 2025-03-12 2025-02-12
2024-01-15 2024-12-11 2024-11-13 2024-10-10 2024-09-11 2024-08-14 2024-07-11 2024-06-12 2024-05-15 2024-04-10 2024-03-12 2024-02-13
2023-01-11 2023-12-12 2023-11-14 2023-10-12 2023-09-13 2023-08-10 2023-07-12 2023-06-13 2023-05-10 2023-04-12 2023-03-14
2022-02-14 2022-01-12 2022-12-13 2022-11-10 2022-10-13 2022-09-13 2022-08-10 2022-07-13 2022-06-10 2022-05-11 2022-04-12 2022-03-10
2021-02-10 2021-01-12 2021-12-10 2021-11-10 2021-10-13 2021-09-14 2021-08-11 2021-07-13 2021-06-10 2021-05-12 2021-04-13 2021-03-10
2020-02-10 2020-01-13 2020-12-10 2020-11-12 2020-10-13 2020-09-11 2020-08-12 2020-07-14 2020-06-10 2020-05-12 2020-04-10 2020-03-11
2019-02-13 2019-01-14 2019-12-11 2019-11-13 2019-10-10 2019-09-12 2019-08-13 2019-07-11 2019-06-12 2019-05-10 2019-04-10 2019-03-12
""".split()
def cpi_date_corrette(seq: list[str]) -> list[str]:
"""Una data di gen/feb che PRECEDE (nell'ordine della pagina, discendente) una data di
marzo-dicembre dello STESSO anno e' la release dell'anno dopo."""
out = []
for i, d in enumerate(seq):
y, m = int(d[:4]), int(d[5:7])
if m <= 2:
j = i + 1
while j < len(seq) and int(seq[j][5:7]) <= 2:
j += 1
if j < len(seq) and int(seq[j][:4]) == y:
d = f"{y + 1}{d[4:]}"
out.append(d)
return sorted(set(out))
def eventi() -> pd.DataFrame:
rows = []
for d in FOMC_PROGRAMMATE:
rows.append(("FOMC", datetime.fromisoformat(f"{d} 14:00").replace(tzinfo=ET)))
for d, hm in FOMC_EMERGENZA:
rows.append(("FOMC", datetime.fromisoformat(f"{d} {hm}").replace(tzinfo=ET)))
for d in cpi_date_corrette(CPI_ARCHIVIO):
rows.append(("CPI", datetime.fromisoformat(f"{d} 08:30").replace(tzinfo=ET)))
ev = pd.DataFrame(rows, columns=["tipo", "t_et"])
ev["t"] = ev["t_et"].map(lambda x: pd.Timestamp(x.astimezone(ZoneInfo("UTC"))))
return ev.sort_values("t").reset_index(drop=True)
# ------------------------------------------------------------------ TP01 orario
def tp01_orario() -> pd.DataFrame:
"""-> DataFrame orario con held_BTC, held_ETH, r_BTC, r_ETH, fee_day (fee del turnover
giornaliero, sulla prima barra del giorno)."""
tp = TrendPortfolio(**CANONICAL)
parts = {}
for a in ASSETS:
h = load_data(a, "1h")
hidx = pd.to_datetime(h["datetime"], utc=True)
r1h = pd.Series(h["close"].values.astype(float), index=hidx).pct_change().fillna(0.0)
d = resample_1d(h)
tgt = tp.target_series(d)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
didx = pd.to_datetime(d["datetime"], utc=True).dt.floor("D")
held_d = pd.Series(held, index=didx)
turn_d = pd.Series(np.abs(np.diff(held, prepend=0.0)) * tp.fee_side, index=didx)
day_of_h = hidx.dt.floor("D")
held_h = held_d.reindex(day_of_h).values
fee_h = np.where(hidx.dt.hour.values == 0, turn_d.reindex(day_of_h).values, 0.0)
parts[a] = pd.DataFrame({f"held_{a}": held_h, f"r_{a}": r1h.values, f"fee_{a}": fee_h}, index=hidx)
J = pd.concat(parts.values(), axis=1, join="inner").dropna()
return J
def pnl(J: pd.DataFrame, flat_mask: np.ndarray | None, fee_side: float) -> pd.Series:
"""Rendimento orario del portafoglio 50/50. `flat_mask` True = barra a esposizione zero
(con fee di chiusura all'ingresso nella finestra e di riapertura all'uscita)."""
out = np.zeros(len(J))
for a in ASSETS:
held = J[f"held_{a}"].values.copy()
fee = J[f"fee_{a}"].values.copy()
if flat_mask is not None:
h0 = held.copy()
held = np.where(flat_mask, 0.0, held)
# transizioni dentro/fuori finestra: |Δheld| × fee, oltre al turnover giornaliero
extra = np.abs(np.diff(held, prepend=held[0])) - np.abs(np.diff(h0, prepend=h0[0]))
fee = fee + np.clip(extra, 0, None) * fee_side
out += 0.5 * (held * J[f"r_{a}"].values - fee)
return pd.Series(out, index=J.index)
def maschera(J: pd.DataFrame, tempi: pd.Series, w_h: float = FINESTRA_H) -> np.ndarray:
"""Barre 1h (open-labeled) che intersecano [t−w, t+w] per almeno un evento."""
idx = J.index.values.astype("datetime64[ns]")
m = np.zeros(len(J), dtype=bool)
w = np.timedelta64(int(w_h * 3600), "s")
one_h = np.timedelta64(3600, "s")
for t in tempi:
t64 = np.datetime64(pd.Timestamp(t).tz_convert("UTC").tz_localize(None))
lo, hi = np.searchsorted(idx, t64 - w - one_h, side="right"), np.searchsorted(idx, t64 + w, side="left")
m[lo:hi] = True
return m
def sharpe_h(s: pd.Series) -> float:
return float(s.mean() / s.std() * np.sqrt(24 * 365.25)) if s.std() > 0 else float("nan")
def sharpe_d(s: pd.Series) -> float:
d = s.groupby(s.index.floor("D")).sum()
return float(d.mean() / d.std() * np.sqrt(365.25)) if d.std() > 0 else float("nan")
def drift_annuo(s: pd.Series) -> float:
return float(s.mean() * 24 * 365.25)
# ------------------------------------------------------------------ misura
def misura() -> dict:
J = tp01_orario()
fee_side = TrendPortfolio(**CANONICAL).fee_side
ev = eventi()
ev = ev[(ev["t"] >= J.index[0]) & (ev["t"] <= J.index[-1])].reset_index(drop=True)
base = pnl(J, None, fee_side)
m_all = maschera(J, ev["t"])
ro = pnl(J, m_all, fee_side)
ro_nofee = pnl(J, m_all, 0.0)
out = dict(n_ore=len(J), da=str(J.index[0]), a=str(J.index[-1]), n_ev=len(ev),
n_fomc=int((ev.tipo == "FOMC").sum()), n_cpi=int((ev.tipo == "CPI").sum()),
ore_finestra=int(m_all.sum()), quota_ore=float(m_all.mean()),
base=dict(sh_d=sharpe_d(base), sh_h=sharpe_h(base), drift=drift_annuo(base)),
riskoff=dict(sh_d=sharpe_d(ro), sh_h=sharpe_h(ro), drift=drift_annuo(ro)),
riskoff_nofee=dict(sh_d=sharpe_d(ro_nofee), drift=drift_annuo(ro_nofee)))
out["d_sh_d"] = out["riskoff"]["sh_d"] - out["base"]["sh_d"]
out["d_drift"] = out["riskoff"]["drift"] - out["base"]["drift"]
# cosa succede DENTRO la finestra (il P&L che il risk-off rinuncia)
dentro = base[m_all]; fuori = base[~m_all]
esposto = (J.loc[m_all, [f"held_{a}" for a in ASSETS]].abs().sum(axis=1) > 0)
out["dentro"] = dict(n=int(m_all.sum()), n_esposte=int(esposto.sum()), media_h=float(dentro.mean()),
t=float(dentro.mean() / dentro.std() * np.sqrt(len(dentro))) if dentro.std() > 0 else float("nan"),
somma=float(dentro.sum()), media_fuori_h=float(fuori.mean()))
# per tipo
out["per_tipo"] = {}
for tipo in ("FOMC", "CPI"):
m = maschera(J, ev.loc[ev.tipo == tipo, "t"])
r = pnl(J, m, fee_side)
out["per_tipo"][tipo] = dict(n=int((ev.tipo == tipo).sum()), d_sh_d=sharpe_d(r) - out["base"]["sh_d"],
d_drift=drift_annuo(r) - out["base"]["drift"], somma_dentro=float(base[m].sum()))
# per anno (M9)
anni = {}
for y, g in base.groupby(base.index.year):
my = m_all[base.index.year == y]
ry = ro[base.index.year == y]
ney = int((ev["t"].dt.year == y).sum())
anni[int(y)] = dict(n_ev=ney, d_sh_d=sharpe_d(ry) - sharpe_d(g), somma_dentro=float(g[my].sum()),
d_cum=float((1 + ry).prod() - (1 + g).prod()))
out["anni"] = anni
# null location-matched (M18): stessi conteggi per anno e stesse ORE del giorno, giorni a caso
rng = np.random.default_rng(SEED)
giorni_per_anno = {y: np.array(sorted(set(J.index[J.index.year == y].normalize()))) for y in J.index.year.unique()}
d_null = np.empty(N_NULL)
for k in range(N_NULL):
t_fake = []
for y, g in ev.groupby(ev["t"].dt.year):
pool = giorni_per_anno.get(int(y))
if pool is None or len(pool) == 0:
continue
days = rng.choice(pool, size=len(g), replace=False)
for d, t in zip(days, g["t"]):
t_fake.append(pd.Timestamp(d) + (t - t.normalize()))
mk = maschera(J, pd.Series(t_fake))
d_null[k] = sharpe_d(pnl(J, mk, fee_side)) - out["base"]["sh_d"]
out["null"] = dict(n=N_NULL, media=float(d_null.mean()), p50=float(np.median(d_null)),
p95=float(np.percentile(d_null, 95)), p05=float(np.percentile(d_null, 5)),
pctl_vero=float((d_null < out["d_sh_d"]).mean() * 100))
# €/giorno all'equity di oggi
out["eur_giorno"] = out["d_drift"] * EQUITY_OGGI / EURUSD / 365.25
return out
def verdetto(m: dict) -> tuple[str, list[str]]:
a = m["d_sh_d"] > 0
b = m["null"]["pctl_vero"] >= 95.0
anni_ok = [y for y, v in m["anni"].items() if v["n_ev"] >= 5]
pos = sum(1 for y in anni_ok if m["anni"][y]["d_sh_d"] > 0)
c = anni_ok and pos / len(anni_ok) >= 0.70
righe = [f"(a) ΔSharpe giornaliero con fee > 0: {m['d_sh_d']:+.3f} → {'PASS' if a else 'FAIL'}",
f"(b) Δ vero al ≥95° pctl del null location-matched: {m['null']['pctl_vero']:.1f}° → {'PASS' if b else 'FAIL'}",
f"(c) Δ > 0 in ≥70% degli anni con ≥5 eventi: {pos}/{len(anni_ok)} → {'PASS' if c else 'FAIL'}"]
return ("LEAD" if (a and b and c) else "REFUTED"), righe
def main() -> int:
print("=" * 88)
print(" r0910 — RISK-OFF ±2h intorno a FOMC e CPI su TP01 (lente oraria, posizione giornaliera)")
print("=" * 88)
m = misura()
print(f" ore: {m['n_ore']} {m['da'][:10]} → {m['a'][:10]} eventi: {m['n_ev']} "
f"(FOMC {m['n_fomc']}, CPI {m['n_cpi']}) ore in finestra: {m['ore_finestra']} = {m['quota_ore']:.2%}")
print(f"\n {'':14s} {'Sh giorn.':>10s} {'Sh orario':>10s} {'drift/anno':>11s}")
for k in ("base", "riskoff"):
v = m[k]; print(f" {k:14s} {v['sh_d']:>10.3f} {v['sh_h']:>10.3f} {v['drift']:>+11.2%}")
v = m["riskoff_nofee"]; print(f" {'riskoff nofee':14s} {v['sh_d']:>10.3f} {'':>10s} {v['drift']:>+11.2%}")
print(f"\n Δ riskoff − base: Sharpe giornaliero {m['d_sh_d']:+.3f} · drift {m['d_drift']:+.2%}/anno "
f"· ≈ {m['eur_giorno']:+.3f} €/giorno a ${EQUITY_OGGI:,.0f}")
d = m["dentro"]
print(f" dentro la finestra: {d['n']} ore ({d['n_esposte']} esposte), media {d['media_h']*1e4:+.2f} bp/h "
f"(fuori {d['media_fuori_h']*1e4:+.2f} bp/h), t = {d['t']:+.2f}, somma {d['somma']:+.2%}")
print("\n per tipo:")
for t, v in m["per_tipo"].items():
print(f" {t:5s} n={v['n']:3d} ΔSh {v['d_sh_d']:+.3f} Δdrift {v['d_drift']:+.2%} P&L dentro {v['somma_dentro']:+.2%}")
print("\n per anno (M9):")
print(f" {'anno':>4s} {'ev':>3s} {'ΔSh':>7s} {'P&L dentro':>11s} {'Δcum':>8s}")
for y, v in m["anni"].items():
print(f" {y:>4d} {v['n_ev']:>3d} {v['d_sh_d']:>+7.3f} {v['somma_dentro']:>+11.2%} {v['d_cum']:>+8.2%}")
n = m["null"]
print(f"\n null location-matched (M18, {n['n']} estrazioni): ΔSh mediana {n['p50']:+.3f} "
f"[p05 {n['p05']:+.3f}, p95 {n['p95']:+.3f}] — il Δ vero sta al {n['pctl_vero']:.1f}° percentile")
v, righe = verdetto(m)
print("\n VERDETTO PRE-REGISTRATO:")
for r in righe:
print(" " + r)
print(f"\n ⇒ {v}")
print("\n non modellato: funding (identico nelle due varianti salvo ~1% delle ore), slippage della "
"finestra (spread largo: se il risultato fosse positivo andrebbe rimisurato col costo vero, C1).")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+36 -9
View File
@@ -58,9 +58,13 @@ REGOLE VINCOLANTI:
5. Se noti qualcosa che merita un controllo umano, dillo in una frase, senza drammatizzare.
6. Massimo %d parole. Prosa, niente elenchi puntati, niente titoli.
Il libro e' TP01 (trend long-flat, 75%%) + SKH01 (breakout, 25%%) su Deribit, capitale ~$635.
Il libro e' TP01 (trend long-flat, 75%%) + SKH01 (breakout, 25%%) su Deribit; il capitale e'
l'equity scritta nella pagina qui sotto, non un valore da ricordare.
Il progetto e' esplicitamente scettico: il suo campione e' corto e il P&L non distingue ancora
l'edge dalla fortuna. Scrivere con quella consapevolezza e' parte del compito.
Un round-trip e' un lotto chiuso in FIFO (tradesdb.fifo_roundtrips): una vendita puo' chiudere
piu' lotti in una volta, quindi vedere piu' round-trip che fill nello stesso giorno e' atteso,
non un'anomalia da segnalare.
""" % MAX_PAROLE
@@ -202,10 +206,26 @@ def testata(voce: dict) -> str:
p, lb = voce.get("pnl") or {}, voce.get("libro") or {}
dq, cum = p.get("delta_equity"), p.get("cumulato_da_arming")
eq = p.get("equity_fine")
# (#35 C8) il cron gira alle 00:30 sul giorno CHIUSO: "oggi" e' ieri -> "nel giorno". E se
# ci sono movimenti di capitale CERTI (mov_giorno; bool() come journal.scorporo: assente o
# 0 -> nessun movimento) la cifra da mostrare e' trading_giorno (al netto), non l'equity
# grezza: altrimenti un versamento si legge come P&L (successo il 25/08, +$1.399 letti come
# profitto). Se trading_giorno non e' calcolabile si resta su delta_equity, ma con l'etichetta
# nuova: non e' "oggi", e non e' comunque il trading al netto.
dq_t = p.get("trading_giorno")
if p.get("mov_giorno") and dq_t is not None:
nota_dq = f" ({dq_t:+.2f} nel giorno, di cui {p['mov_giorno']:+.2f} movimenti)"
elif dq is not None:
nota_dq = f" ({dq:+.2f} nel giorno)"
else:
nota_dq = ""
r = [f"<b>📓 Giornale — {_b(voce['giorno'])}</b>"]
r.append(" equity " + (f"<b>${eq:,.2f}</b>" if eq is not None else "n/d")
+ ("" if dq is None else f" ({dq:+.2f} oggi)")
+ ("" if cum is None else f" · dall'arming {cum:+.2f}"))
+ nota_dq
+ ("" if cum is None else f" · dall'arming {cum:+.2f}"
+ (f" (di cui {p['mov_arming']:+.2f} versati -> trading "
f"{p['trading_da_arming']:+.2f})"
if p.get("mov_arming") and p.get("trading_da_arming") is not None else "")))
if "errore" not in lb and lb.get("asset"):
pos = " · ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted(lb["asset"].items()))
r.append(f" {pos} · leva {lb.get('leva_lorda') or 0:.2f}x")
@@ -221,15 +241,22 @@ def per_telegram(voce: dict, analisi: str, stato: str, motivi: list[str] | None
"""
testo = testata(voce)
if analisi.strip():
corpo = _b(analisi.strip())
corpo = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", corpo) # il grassetto del giornale
grezzo = analisi.strip()
if stato == "sospetta":
testo += "\n\n⚠️ <i>controllo sui numeri: sospetta — " + _b("; ".join(motivi or [])) + "</i>"
testo += "\n\n" + corpo
# (#35 C13) il taglio va sul GREZZO, PRIMA di _b (html.escape) e PRIMA di **-><b>: un
# taglio sul testo gia' composto puo' spezzare un'entita' (es. "&am") o lasciare un <b>
# aperto, e Telegram rifiuta l'INTERO messaggio ("can't parse entities") — il taglio si
# perde tutto, non si accorcia. Il marcatore si aggiunge dopo, fuori dall'escape (e'
# testo nostro, senza `&`/`<`: non ha bisogno di escape).
taglio = "\n\n<i>[…tagliato: la voce intera e' in docs/journal/%s.md]</i>" % voce["giorno"]
budget = LIMITE_TELEGRAM - len(testo) - len("\n\n") - len(taglio)
tagliata = len(grezzo) > budget
if tagliata:
grezzo = grezzo[:max(budget, 0)]
corpo = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", _b(grezzo)) # il grassetto del giornale
testo += "\n\n" + corpo + (taglio if tagliata else "")
else:
testo += ("\n\n⚠️ <i>analisi non disponibile (" + _b(stato) + "): "
+ _b("; ".join(motivi or ["motivo non registrato"])) + "</i>")
if len(testo) > LIMITE_TELEGRAM:
taglio = "\n\n<i>[…tagliato: la voce intera e' in docs/journal/%s.md]</i>" % voce["giorno"]
testo = testo[:LIMITE_TELEGRAM - len(taglio)] + taglio
return testo
+42 -5
View File
@@ -4,8 +4,8 @@ TP01 e SKH01 tradano lo STESSO strumento (BTC/ETH _USDC-PERPETUAL). Su Deribit e
posizione netta per strumento per conto -> non si possono tenere due gambe separate: si combinano in
software in un unico target netto, e si manda UN ordine per asset per raggiungerlo.
Formula (preserva il budget di rischio $300/asset, split 75/25; coerente col blend di ritorni
deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
Formula (preserva il budget di rischio per-asset di config (CAP sotto: `max_notional_per_asset_usd`
o `_frac`), split 75/25; coerente col blend di ritorni deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
net_target_usd[asset] = clamp( WEIGHT * E * (W_TP01 * tp_frac + W_SKH * skh_sign), ±CAP )
@@ -13,8 +13,8 @@ deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target
skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m)
E = equity reale del conto (fallback paper se offline)
CAP = cap notional per-asset. Fisso = max_notional_per_asset_usd ($300), OPPURE
DINAMICO = equity * max_notional_per_asset_frac (es. 0.5 -> equity/2) se la
CAP = cap notional per-asset. Fisso = `max_notional_per_asset_usd` di config, OPPURE
DINAMICO = equity * `max_notional_per_asset_frac` (es. 0.5 -> equity/2) se la
frazione e' presente in config -> il cap cresce col capitale e non strozza il
book quando depositi (frontiera 2026-07-03). Dinamico SOLO con equity reale
fidata; su fallback/offline si torna al cap fisso (protezione downside).
@@ -136,6 +136,42 @@ def _scala(real_equity: float | None = None, eq_fallback: str | None = None,
return s
def leva_tetto(cfg: dict | None = None) -> tuple[float, str]:
"""Leva lorda massima che il libro PUO' prendere. UNA formula, non tre (#28): prima era
ridichiarata in `usde.frazione_cuscino` (N_ASSET fisso, senza scala) e nella regola `leva` del
giornale (senza scala) -> a scala 1,25 il cuscino USDC risultava 0,30 dell'equity invece di
0,375, e la regola `leva` mostrava un tetto diverso da quello che il libro rispetta davvero.
leva = len(ASSETS) x min(WEIGHT, frac) x scala, al piu' LEVA_LORDA_MAX. -> (leva, come).
- frac = cfg["max_notional_per_asset_frac"] se presente, altrimenti WEIGHT. Il min(WEIGHT, frac)
viene dal fatto che `book_net_target` non supera mai weight*equity*(W_TP01+W_SKH) =
weight*equity (i due pesi sommano a 1): |raw| <= weight*equity, quindi |net_target| <=
scala*min(cap, weight*equity) -- un frac oltre WEIGHT alza il cap nominale ma non la leva
REALE che il libro puo' esprimere.
- scala = la scala DICHIARATA (`_scala(real_equity=1.0, cfg=cfg)`, gia' validata): un tetto
descrive quanto la config AUTORIZZA, non quanto un giro con equity illeggibile ha preso.
- Su `ScalaNonAutorizzata` (config fuori scaletta o fuori tetto) NON si solleva: un
sorvegliante (`cuscino_watch`) non deve morire proprio quando la config e' sbagliata
(verifica fable B5) -> si ritorna il tetto di CODICE, dichiarando l'anomalia in `come`.
"""
if cfg is None:
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
try:
scala = _scala(real_equity=1.0, cfg=cfg)
except ScalaNonAutorizzata as e:
breve = str(e).splitlines()[0]
breve = breve if len(breve) <= 80 else breve[:77] + "..."
return LEVA_LORDA_MAX, (f"config fuori scaletta/tetto ({breve}): "
f"tetto di codice LEVA_LORDA_MAX")
raw_frac = cfg.get("max_notional_per_asset_frac")
frac = float(raw_frac) if raw_frac is not None else WEIGHT
frac_eff = min(WEIGHT, frac)
leva = len(ASSETS) * frac_eff * scala
base = f"{len(ASSETS)} asset x frac {frac_eff} x scala {scala} = {leva:.2f}x"
if leva > LEVA_LORDA_MAX + 1e-12:
return LEVA_LORDA_MAX, f"{base}, troncato al tetto di codice {LEVA_LORDA_MAX}x"
return leva, base
def _read_watermark() -> float | None:
"""Ultima equity REALE osservata sul conto. None se mai vista o file illeggibile."""
@@ -269,6 +305,7 @@ def book_report(offline: bool = False, equity_override: float | None = None,
live_feed: se True usa il feed 5m fresco effimero per SKH01 (src/live/livefeed.fresh_5m) ->
segnale 230m all'ultima barra chiusa, per l'esecuzione reale. Default False (feed certificato,
deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero)."""
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
sh = shadow_report(offline=offline, equity_override=equity_override)
equity = sh["equity"]
# registra l'equity reale quando e' leggibile: e' cio' che rende sicuro il cap di
@@ -315,7 +352,7 @@ def book_report(offline: bool = False, equity_override: float | None = None,
net = book_net_target(tp_frac, sign, equity, cap, scala=scala)
cur = float(a_rec["position_usd"])
mark = a_rec["mark"]
order = build_book_order(inst, net, cur, mark, min_usd=5.0)
order = build_book_order(inst, net, cur, mark, min_usd=float(cfg.get("min_order_usd", 5.0)))
if order:
orders.append(order)
assets.append(dict(
+66
View File
@@ -0,0 +1,66 @@
"""Argomenti da riga di comando degli script operativi: un flag ignoto NON e' l'azione di default.
PERCHE' ESISTE (2026-09-02). Nessuno degli script di `scripts/live/` usava argparse: leggevano
`sys.argv` con `in` e `index`, quindi un flag sbagliato non era un errore — era il ramo `else`.
Costo misurato lo stesso giorno, durante una revisione: `trades_db.py --help` non ha stampato
l'uso, e' caduto in `sync()` e ha riscritto `meta.ultimo_sync`. Gli altri due sono peggio:
`journal.py --help` avrebbe scritto la pagina di oggi e una riga nel DB, `analista.py --help`
avrebbe speso una chiamata al modello e mandato un messaggio Telegram.
TRE REGOLE, e nient'altro:
1. **Nessun argparse.** Gli script hanno flag posizionali-per-nome (`--report`, `--secco`) e
flag con valore (`--giorno 2026-08-21`); argparse cambierebbe i messaggi d'errore, il
codice d'uscita e il comportamento di `--help` di 18 script che il cron gia' chiama. Qui
serve il contrario: **lasciare intatto cio' che gira** e rifiutare cio' che non e' previsto.
2. **Il cron non deve cambiare.** I flag dichiarati sono quelli che gli script gia' accettano:
`valida()` non ne aggiunge e non ne toglie (P15: una regola si prova contro il codice che
dovrebbe eseguirla). L'unica novita' e' `--help`, che prima non esisteva.
3. **Si esce PRIMA di qualunque effetto.** `valida()` va chiamata come prima istruzione del
blocco `__main__`, prima di `connect()`, prima del sync, prima della rete.
Codici d'uscita: 0 per `--help` (l'utente ha chiesto l'uso), 2 per un flag ignoto (errore d'uso,
distinto dal 1 con cui gli script segnalano un esito negativo del loro lavoro).
"""
from __future__ import annotations
import sys
class UsoSbagliato(Exception):
"""Flag non previsto da questo script. Portato con l'elenco di cio' che e' previsto (P4)."""
def valida(nome: str, uso: str, *, flag: tuple[str, ...] = (), con_valore: tuple[str, ...] = (),
argv: list[str] | None = None, esci: bool = True) -> list[str]:
"""Controlla `argv` contro i flag dichiarati. Ritorna gli argomenti; solleva o esce se ignoti.
`flag` sono i booleani (`--quiet`), `con_valore` quelli che consumano l'argomento seguente
(`--giorno 2026-08-21`). `--help`/`-h` stampano `uso` ed escono 0. Con `esci=False` non
tocca `sys.exit` e solleva `UsoSbagliato`: e' la forma che i test usano.
"""
args = list(sys.argv[1:] if argv is None else argv)
if "--help" in args or "-h" in args:
if not esci:
return args
print(uso.strip())
raise SystemExit(0)
noti, ignoti, attesa_valore = set(flag) | set(con_valore), [], False
for a in args:
if attesa_valore: # e' il valore di un flag noto, non un flag
attesa_valore = False
continue
if not a.startswith("-"):
ignoti.append(a); continue # posizionale: nessuno di questi script ne ha
if a not in noti:
ignoti.append(a); continue
attesa_valore = a in con_valore
if attesa_valore:
ignoti.append(f"{args[-1]} (manca il valore)")
if ignoti:
msg = (f"{nome}: argomento non previsto: {', '.join(ignoti)}\n"
f" previsti: {' '.join(sorted(noti)) or '(nessuno)'}\n\n{uso.strip()}")
if not esci:
raise UsoSbagliato(msg)
print(msg, file=sys.stderr)
raise SystemExit(2)
return args
+37 -17
View File
@@ -1,13 +1,16 @@
"""Accesso Deribit MAINNET in SOLA LETTURA (via Cerbero MCP) + costruttore ordini deterministico.
"""Accesso Deribit MAINNET (via Cerbero MCP) + costruttore ordini deterministico. La classe
`DeribitRead` qui e' in SOLA LETTURA (nessun metodo di trading, by design); l'esecuzione REALE la
estende in un modulo separato (`src.live.execution.DeribitTrader`) — non e' piu' un progetto
futuro (#35 A9): e' ARMATA e LIVE su Deribit mainnet dal 2026-06-23 (`execution_enabled` in
config/live.json), usata da `scripts/live/book_execute.py`.
Serve lo SHADOW MODE di TP01 (`scripts/live/live_trend.py`): legge prezzi / conto / posizioni REALI
dal mainnet (token `.env.mainnet`) e costruisce gli ordini di ribilancio **senza inviarli**. Qui NON
esiste alcun metodo di trading — by design: l'unica via per piazzare ordini sara' un modulo separato,
abilitato esplicitamente, dopo la validazione shadow + micro-test.
Da qui vengono prezzi / conto / posizioni REALI (token `.env.mainnet`): li usa sia il libro live
(TP01+SKH01, via src.live.book + execution) sia lo shadow mode e i monitor di sorveglianza.
Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet reale,
e in questa fase solo in lettura. I contratti sono ristretti a BTC/ETH-PERPETUAL (inverse: `amount`
in USD notional, step verificato su Deribit: BTC $10, ETH $1).
Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet
reale. I contratti ESEGUITI sono i perp LINEARI USDC (`_USDC-PERPETUAL`, non inverse): `amount` in
base-coin (BTC/ETH), margine e P&L in USDC — vedi `_CONTRACT` sotto per step/tick/minimi per
strumento.
"""
from __future__ import annotations
@@ -236,11 +239,22 @@ class DeribitRead:
return out.get("positions", []) if isinstance(out, dict) else []
def position_usd(self, instrument: str) -> float:
"""Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat."""
"""Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat.
⚠️ Il gateway manda `size`/`size_currency` in ASSOLUTO e il segno a parte in `direction`
(#17): letta com'era, uno short risultava un long, e close()/ensure_disaster_sl decidevano
lato ordine e prezzo dello stop dal segno sbagliato. `direction` assente o `"zero"` -> ci si
fida di `size` cosi' com'e' (compatibile con un gateway che un giorno mandasse gia' il segno)."""
cur = _CONTRACT[instrument]["settle"]
for p in self.positions(cur):
if p.get("instrument_name") == instrument or p.get("instrument") == instrument:
return float(p.get("size") or p.get("size_currency") or 0.0)
s = float(p.get("size") or p.get("size_currency") or 0.0)
direction = str(p.get("direction") or "").lower()
if direction in ("sell", "short"):
return -abs(s)
if direction in ("buy", "long"):
return abs(s)
return s
return 0.0
def trade_history(self, instrument: str, limit: int = 20) -> list[dict]:
@@ -251,17 +265,23 @@ class DeribitRead:
def open_orders(self, instrument: str) -> list[dict]:
"""Ordini APERTI su `instrument` (limit resting + trigger). Deribit puo' omettere i trigger
untriggered da type='all' -> interroga anche 'trigger_all' e fa merge per order_id."""
untriggered da type='all' -> interroga anche 'trigger_all' e fa merge per order_id.
⚠️ Se UNA delle due chiamate solleva, NON si ingoia (#24): prima ritornava [] anche a meta'
letta, e un bracket disaster-SL perso su 'trigger_all' e' lo stesso difetto di una posizione
persa su 'all' — il chiamante (ensure_disaster_sl) deve saperlo, non decidere su dati a
meta'. Il chiamante che vuole restare in piedi comunque (shadow.shadow_report) si mette il
try/except da solo."""
cur = _CONTRACT[instrument]["settle"]
seen: dict = {}
for typ in ("all", "trigger_all"):
try:
out = self._unwrap(self._post("/mcp-deribit/tools/get_open_orders",
{"currency": cur, "type": typ}))
lst = out if isinstance(out, list) else (out.get("orders", []) if isinstance(out, dict) else [])
for o in lst:
if o.get("instrument_name") == instrument or o.get("instrument") == instrument:
seen[o.get("order_id")] = o
except Exception:
pass
except Exception as e: # noqa: BLE001 — si registra il tipo (P3), non si ingoia (#24)
raise RuntimeError(f"open_orders illeggibile: {type(e).__name__}: {e}") from e
lst = out if isinstance(out, list) else (out.get("orders", []) if isinstance(out, dict) else [])
for o in lst:
if o.get("instrument_name") == instrument or o.get("instrument") == instrument:
seen[o.get("order_id")] = o
return list(seen.values())
+99 -39
View File
@@ -4,12 +4,17 @@ Estende DeribitRead (sola lettura) coi metodi di trading, con la logica PROVATA
(Old/src/live/execution.py): entrata market verificata (state=='filled' + trade riscontrati, fill/fee
reali, filled_amount autorevole), uscita market reduce_only, disaster-bracket STOP_MARKET reduce_only.
GUARDRAIL: solo strumenti in ALLOWED; cap di size SOLO sulle APERTURE (MAX_AMOUNT). Le CHIUSURE si
tentano SEMPRE senza cap (principio di sicurezza di Old: si deve poter uscire da qualunque posizione).
Nessun parametro di leva (Deribit non la accetta per-ordine: l'esposizione la decide la SIZE).
GUARDRAIL: solo strumenti in ALLOWED; cap di size SOLO sulle APERTURE — `set_open_cap()` lo deriva
per-strumento dal cap di config del libro (config/live.json, #18); MAX_AMOUNT resta il tetto di
DEFAULT per chi non lo chiama (il micro-test, size fissa minima). Le CHIUSURE si tentano SEMPRE
senza cap (principio di sicurezza di Old: si deve poter uscire da qualunque posizione). Nessun
parametro di leva (Deribit non la accetta per-ordine: l'esposizione la decide la SIZE).
⚠️ INVIA ORDINI REALI CON SOLDI VERI. Finestra d'uso attuale: micro-test (scripts/live/microtest.py).
Il deploy pieno di TP01 resta gated finche' il percorso live non e' abilitato esplicitamente.
⚠️ INVIA ORDINI REALI CON SOLDI VERI. Il libro (TP01+SKH01) e' ARMATO e LIVE su Deribit mainnet dal
2026-06-23 (commit db738bc): questo NON e' un percorso di micro-test, e' l'esecuzione di produzione
usata da `scripts/live/book_execute.py` (cron del book). Il micro-test
(scripts/live/microtest.py) resta un percorso a parte, sotto lo stesso guardrail, col cap di
DEFAULT (MAX_AMOUNT).
"""
from __future__ import annotations
@@ -17,11 +22,14 @@ from dataclasses import dataclass
from src.live.deribit import DISASTER_LABEL, DeribitRead, disaster_stop_price, notional_to_amount, quantize_price
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger
# (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello
# lo decide config/live.json max_notional_per_asset_usd). Il micro-test invia comunque size fissa minima.
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger di
# DEFAULT (in coin, sotto), usato SOLO quando nessuno ha chiamato set_open_cap() (il micro-test,
# che invia comunque size fissa minima). Sul libro il tetto per apertura lo deriva set_open_cap()
# dal cap di config (config/live.json max_notional_per_asset_usd, #18): backstop sopra il sizing
# di TP01/book, non il sizing operativo. (#35 A9: via il "~$630/$430 su conto ~$600" cablato —
# MAX_AMOUNT e' in coin, non in dollari, e non e' il tetto operativo da quando esiste set_open_cap.)
ALLOWED = {"BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"}
MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25}
MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25} # DEFAULT — vedi set_open_cap()
FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat
@@ -29,6 +37,19 @@ class GuardrailError(RuntimeError):
pass
class RebalanceParziale(RuntimeError):
"""`rebalance_signed` ha eseguito almeno un fill (es. il close di un flip) e POI un passo
successivo ha sollevato (es. l'open dall'altro lato). Il fill gia' eseguito NON e' un fill
perso: `.fills` li porta al chiamante (che li registra) e `.causa` e' l'eccezione originale
(il chiamante la rilancia dentro il suo handler di isolamento, #23)."""
def __init__(self, fills: list["Fill"], causa: Exception):
self.fills = fills
self.causa = causa
super().__init__(f"rebalance parziale: {len(fills)} fill eseguiti, poi "
f"{type(causa).__name__}: {causa}")
@dataclass
class Fill:
"""Esito verificato di un ordine reale."""
@@ -55,6 +76,20 @@ def _avg_price(order: dict, trades: list[dict]) -> float | None:
class DeribitTrader(DeribitRead):
"""Trading minimo e verificato. Apre/chiude solo entro i guardrail; le chiusure sempre."""
def __init__(self) -> None:
super().__init__()
self._open_cap: dict[str, float] = {} # per-strumento, da set_open_cap() (#18)
def set_open_cap(self, instrument: str, usd_cap: float, mark: float) -> None:
"""Tetto di apertura per `instrument`, derivato dal cap di config del libro (#18):
amount_max = 1,10 * usd_cap / mark — 10% sopra il cap nozionale per non rifiutare
un'apertura legittima per un arrotondamento di sizing/mark. Backstop anti-fat-finger
sopra il sizing di TP01/book, non il sizing operativo (quello resta config/live.json).
Senza chiamata, `_submit` resta sul DEFAULT MAX_AMOUNT (il micro-test)."""
if usd_cap <= 0 or mark <= 0:
raise ValueError(f"set_open_cap richiede usd_cap>0 e mark>0 (usd_cap={usd_cap}, mark={mark})")
self._open_cap[instrument] = 1.10 * usd_cap / mark
def _submit(self, instrument: str, side: str, amount: float, *, reduce_only: bool,
label: str, order_type: str = "market", price: float | None = None) -> Fill:
if instrument not in ALLOWED:
@@ -62,7 +97,9 @@ class DeribitTrader(DeribitRead):
if side not in ("buy", "sell"):
raise GuardrailError(f"side non valido: {side}")
if not reduce_only: # cap SOLO sulle aperture; le chiusure si tentano sempre
cap = MAX_AMOUNT.get(instrument, 0.0)
# getattr, non self._open_cap diretto: un DeribitTrader creato con object.__new__
# (test che saltano __init__) non ha l'attributo, e qui non deve dare AttributeError.
cap = getattr(self, "_open_cap", {}).get(instrument, MAX_AMOUNT[instrument])
if amount <= 0 or amount > cap:
raise GuardrailError(f"size {amount} fuori dal cap apertura (0, {cap}]")
if amount <= 0:
@@ -150,36 +187,46 @@ class DeribitTrader(DeribitRead):
- target ~0 -> close();
- stesso segno, |target|<|cur| -> REDUCE reduce_only;
- apertura/aumento -> open buy/sell (capped dal guardrail apertura).
Ritorna i Fill eseguiti."""
Ritorna i Fill eseguiti.
⚠️ FILL PERSI (#23). Un flip e' DUE passi (close, poi open dall'altra parte): se il primo
va a segno e il secondo solleva, il fill del primo passo NON deve sparire. Da qui in poi
un'eccezione con almeno un fill gia' in `fills` diventa `RebalanceParziale(fills, causa)`
invece di propagare nuda: il chiamante registra i fill fatti e poi rilancia `causa`."""
cur = self.position_usd(instrument)
if abs(target_notional_usd - cur) < min_usd:
return []
fills: list[Fill] = []
crossing = (cur > FLAT_USD and target_notional_usd < -FLAT_USD) or \
(cur < -FLAT_USD and target_notional_usd > FLAT_USD)
if crossing: # flip: flatta, poi riparti da zero
f = self.close(instrument, label="book-flip-close")
if f:
fills.append(f)
cur = 0.0
if abs(target_notional_usd) < FLAT_USD: # target flat -> esci (sempre permessa)
if abs(cur) > FLAT_USD:
f = self.close(instrument, label="book-exit")
try:
crossing = (cur > FLAT_USD and target_notional_usd < -FLAT_USD) or \
(cur < -FLAT_USD and target_notional_usd > FLAT_USD)
if crossing: # flip: flatta, poi riparti da zero
f = self.close(instrument, label="book-flip-close")
if f:
fills.append(f)
cur = 0.0
if abs(target_notional_usd) < FLAT_USD: # target flat -> esci (sempre permessa)
if abs(cur) > FLAT_USD:
f = self.close(instrument, label="book-exit")
if f:
fills.append(f)
return fills
delta = target_notional_usd - cur
amount = notional_to_amount(instrument, abs(delta), price=mark)
if amount <= 0:
return fills
same_sign = (target_notional_usd > 0) == (cur > 0)
if cur != 0.0 and same_sign and abs(target_notional_usd) < abs(cur):
side = "sell" if cur > 0 else "buy" # riduci nello stesso verso, reduce_only
fills.append(self._submit(instrument, side, amount, reduce_only=True, label="book-reduce"))
else:
side = "buy" if target_notional_usd > 0 else "sell" # apri/aumenta verso il target
fills.append(self.open(instrument, side, amount, label="book-open"))
return fills
delta = target_notional_usd - cur
amount = notional_to_amount(instrument, abs(delta), price=mark)
if amount <= 0:
return fills
same_sign = (target_notional_usd > 0) == (cur > 0)
if cur != 0.0 and same_sign and abs(target_notional_usd) < abs(cur):
side = "sell" if cur > 0 else "buy" # riduci nello stesso verso, reduce_only
fills.append(self._submit(instrument, side, amount, reduce_only=True, label="book-reduce"))
else:
side = "buy" if target_notional_usd > 0 else "sell" # apri/aumenta verso il target
fills.append(self.open(instrument, side, amount, label="book-open"))
return fills
except Exception as e: # noqa: BLE001 — un fill gia' eseguito non e' un fill perso (#23)
if fills:
raise RebalanceParziale(fills, e) from e
raise
# --- DISASTER BRACKET (assicurazione on-book per outage; da Old) ---
def place_disaster_sl(self, instrument: str, side_held: str, amount: float,
@@ -194,10 +241,20 @@ class DeribitTrader(DeribitRead):
return self._unwrap(self._post("/mcp-deribit/tools/cancel_order", {"order_id": order_id})) or {}
def ensure_disaster_sl(self, instrument: str, sl_pct: float) -> dict:
"""Garantisce UN disaster-SL coerente con la posizione (lifecycle completo, idempotente):
- flat -> cancella eventuali bracket orfani;
- long -> assicura UN solo STOP_MARKET reduce_only a ~-sl_pct, size = posizione;
- gia' coerente (1 bracket, amount~=, stop entro 5%) -> lascia com'e' (niente churn/gap).
"""Garantisce UN disaster-SL coerente con la posizione (lifecycle completo, idempotente).
Stati possibili (`dict["state"]`):
- `"flat"` -> nessuna posizione: cancella eventuali bracket orfani;
- `"ok"` -> gia' coerente (1 bracket, amount~=, stop entro 5%): lascia com'e';
- `"placed"` -> ricostruito (cancellato l'incoerente e ripiazzato) al primo o secondo
tentativo, e il piazzamento e' verificato;
- `"place-failed"` -> il ripiazzamento e' partito ma NON e' verificato (state/trade non
confermano): l'ordine potrebbe non essere a book;
- `"naked"` -> il bracket incoerente e' stato cancellato e il ripiazzamento e'
fallito (eccezione) 2 volte di fila: la posizione e' SENZA stop;
- `"unknown"` -> posizione o open_orders illeggibili (#24): NON cancella ne' piazza —
decidere alla cieca su meta' dei dati e' peggio che aspettare il giro
dopo. `notes` porta tipo ed errore della lettura fallita.
⚠️ LA FINESTRA SCOPERTA (riparata 2026-08-25). Il ramo di ricostruzione **cancella prima
e ripiazza dopo**: fra le due chiamate la posizione e' senza alcuno stop. Finche' il
@@ -216,8 +273,11 @@ class DeribitTrader(DeribitRead):
non basta per cambiare il ciclo di vita dei bracket su un percorso con soldi veri senza
misurarlo. Riparato il silenzio, non toccata la sequenza.
"""
pos = self.position_usd(instrument)
brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == DISASTER_LABEL]
try:
pos = self.position_usd(instrument)
brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == DISASTER_LABEL]
except Exception as e: # noqa: BLE001 — si registra il motivo (P3); non si cancella ne' piazza (#24)
return {"state": "unknown", "notes": f"{type(e).__name__}: {e}"}
if abs(pos) < FLAT_USD:
for o in brackets:
self.cancel_order(o.get("order_id"))
+35
View File
@@ -0,0 +1,35 @@
"""registra_giro — il giro che un sorvegliante ha misurato, come riga jsonl (2026-09-21, #16).
PERCHE' ESISTE. `cc01_regime_watch` e' morto 14 notti di fila (#10) senza che nessuno lo vedesse,
perche' non scriveva niente da sorvegliare: `monitor_health` guarda solo le serie che i monitor
scrivono, e un monitor silenzioso non e' distinguibile da uno che dice "tutto ok". La riparazione
di cc01 (`data/cc01_regime/runs.jsonl`, commit 31f5915) e' il modello: qui si generalizza a ogni
sorvegliante, cosi' un monitor fermo diventa FERMO in `monitor_health`, non invisibile.
Un registro che fallisce non deve cambiare l'esito del sorvegliante che lo scrive: `registra_giro`
non solleva mai, e su errore lo dice su stderr e ritorna False.
"""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
def registra_giro(path: Path, **campi) -> bool:
"""Appende `{"ts": <ms epoch>, **campi}` a `path` (jsonl, una riga). Crea la cartella se manca.
Ritorna True se la riga e' stata scritta, False su OSError (disco pieno, permessi, percorso
invalido...) dopo aver scritto il motivo su stderr. Non solleva mai: la misura del
sorvegliante e' gia' fatta, e un registro che fallisce non deve buttarla via.
"""
riga = {"ts": int(time.time() * 1000), **campi}
try:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as fh:
fh.write(json.dumps(riga, default=str) + "\n")
return True
except OSError as e:
print(f"registra_giro: non scrivo {path}: {type(e).__name__}: {e}", file=sys.stderr)
return False
+209 -48
View File
@@ -25,6 +25,12 @@ from src.live import tradesdb as T
PROJECT_ROOT = Path(__file__).resolve().parents[2]
JOURNAL_DIR = PROJECT_ROOT / "docs" / "journal"
# Movimenti di capitale DICHIARATI dall'operatore (append-only, dentro il perimetro di backup —
# P11). Servono per i movimenti che il rilevatore non puo' vedere: sotto `EQUITY_JUMP_ALERT`
# (il versamento di prova da ~$25 del 2026-08-25 08:00Z, +3,3% su $647, restato nel P&L per
# 12 giorni) o dentro un'ora di mercato non misurabile. Una riga per movimento:
# {"ts_utc": "...", "delta_usd": 24.9, "banda_usd": 0.5, "nota": "...", "dichiarato_il": "..."}
MOVIMENTI_DICHIARATI = PROJECT_ROOT / "data" / "live" / "movimenti_dichiarati.jsonl"
ASSETS = ("BTC", "ETH")
# Gli orizzonti del segnale TP01 in produzione (trend_portfolio: TSMOM 30/90/180 giorni).
ORIZZONTI = (30, 90, 180)
@@ -118,7 +124,34 @@ def _px_1h(asset: str) -> pd.Series | None:
return None
def movimenti_capitale(con, fino_ts: str) -> dict:
def movimenti_dichiarati(path: Path | None = None) -> tuple[list[dict], list[str]]:
"""Righe di `MOVIMENTI_DICHIARATI` -> (movimenti validi, errori di riga). Mai solleva.
Una riga rotta non azzera le altre e NON sparisce: torna in `errori` (P3 — si registra nel
punto in cui si ingoia)."""
path = MOVIMENTI_DICHIARATI if path is None else path
if not path.exists():
return [], []
validi, errori = [], []
for n, riga in enumerate(path.read_text().splitlines(), 1):
if not riga.strip():
continue
try:
d = json.loads(riga)
ts = pd.Timestamp(d["ts_utc"])
if ts.tzinfo is None:
raise ValueError("ts_utc senza fuso")
delta = float(d["delta_usd"])
if delta == 0:
raise ValueError("delta_usd zero")
validi.append(dict(ts=ts, delta=delta, banda=float(d.get("banda_usd") or 0.0),
nota=str(d.get("nota") or ""), dichiarato_il=d.get("dichiarato_il")))
except Exception as e: # noqa: BLE001 — la riga rotta si riporta
errori.append(f"{path.name}:{n}: {type(e).__name__}: {e}")
validi.sort(key=lambda d: d["ts"])
return validi, errori
def movimenti_capitale(con, fino_ts: str, rows=None) -> dict:
"""Salti di equity fra letture consecutive: versamenti e prelievi, da scorporare dal P&L.
PERCHE' ESISTE. Il P&L di giornale e' un delta di equity, quindi un versamento ci finisce
@@ -137,45 +170,113 @@ def movimenti_capitale(con, fino_ts: str) -> dict:
⚠️ LIMITE DICHIARATO (D5): un movimento sotto soglia (es. $150 su un conto da $2.000)
non si distingue dal mercato e RESTA nel P&L. E' lo stesso limite del rilevatore live.
Il rimedio NON e' abbassare la soglia (ogni ora di mercato diventerebbe un candidato): e'
la DICHIARAZIONE dell'operatore in `MOVIMENTI_DICHIARATI` — l'unica fonte che sa l'importo.
Un movimento dichiarato e' `movimento` con `fonte="dichiarato"`, l'importo e' quello
dichiarato (non il salto di equity), e `dopo` = `prima` + importo: il P&L di mercato di
quell'ora resta nel rendimento invece di uscire con il movimento. Se coincide con un salto
rilevato, l'importo dichiarato VINCE sul salto (fonte "dichiarato+rilevato"); un movimento
dichiarato fuori dalle letture, o dopo `fino_ts`, non entra e finisce in `avvisi`.
"""
from src.live import book as _book # lazy: prende la soglia viva, anche monkeypatchata
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall()
# `rows` si puo' passare gia' letto: due SELECT sulla stessa tabella a un'ora del cron
# possono cadere ai due lati di una scrittura e descrivere due istanti diversi.
if rows is None:
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall()
soglia = float(_book.EQUITY_JUMP_ALERT)
candidati = [(a, b) for a, b in zip(rows, rows[1:])
if a["equity"] and abs(b["equity"] - a["equity"]) / a["equity"] >= soglia]
if not candidati:
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia)
dichiarati, avvisi = movimenti_dichiarati()
if not candidati and not dichiarati:
return dict(eventi=[], certi=0.0, ambigui=0.0, soglia=soglia, avvisi=avvisi)
try:
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
leva_tetto = float(cfg["max_notional_per_asset_frac"]) * len(ASSETS)
# la leva lorda massima che il libro puo' tenere: UNA sola formula, in `book.leva_tetto`
# (P1: derivata da `book`, non ridichiarata — #28 ne aveva tre diverse fra qui,
# usde.frazione_cuscino e la regola `leva` sotto, e non tornavano)
leva_tetto = _book.leva_tetto(cfg)[0]
except Exception:
leva_tetto = 1.0 # il tetto di progetto: bound conservativo, usato solo qui
px = {a: _px_1h(a) for a in ASSETS}
leva_tetto = float(_book.LEVA_LORDA_MAX) # config illeggibile: il tetto di codice
px = {a: _px_1h(a) for a in ASSETS} if candidati else {}
eventi = []
for prev, cur in candidati:
t0, t1 = pd.Timestamp(prev["ts_utc"]), pd.Timestamp(cur["ts_utc"])
delta = float(cur["equity"] - prev["equity"])
pct = delta / prev["equity"]
mosse = []
# Il mercato e' MISURABILE solo se il feed copre entrambe le letture. Il feed 1h
# certificato si ferma alle 00:00 (rebuild alle 00:30): per il resto del giorno
# `asof` restituirebbe la STESSA barra per t0 e t1, max_mkt 0, e qualunque calo >=10%
# diventerebbe un "movimento" — un crash stampato come prelievo (revisione 02/09).
# Non misurabile => `ambiguo` con la ragione (P5: "non vedo" non e' "mercato fermo").
mosse, non_mis = [], []
for a in ASSETS:
s = px.get(a)
if s is None or s.empty:
continue
non_mis.append(f"{a}: feed 1h non leggibile"); continue
fine = s.index[-1] + pd.Timedelta(hours=1) # l'ultima barra copre un'ora
if t0 < s.index[0] or t1 > fine:
non_mis.append(f"{a}: feed 1h fermo a {s.index[-1]:%Y-%m-%dT%H:%M}Z"); continue
p0, p1 = s.asof(t0), s.asof(t1)
if p0 and p1 and not (pd.isna(p0) or pd.isna(p1)):
mosse.append(abs(float(p1) / float(p0) - 1.0))
max_mkt = max(mosse) * leva_tetto if mosse else None
classe = "movimento" if (max_mkt is not None and abs(pct) > 2.0 * max_mkt) else "ambiguo"
if pd.isna(p0) or pd.isna(p1) or not p0 or not p1:
non_mis.append(f"{a}: barra assente"); continue
mosse.append(abs(float(p1) / float(p0) - 1.0))
if non_mis:
max_mkt, classe, nota = None, "ambiguo", "mercato non misurabile: " + "; ".join(non_mis)
else:
max_mkt = max(mosse) * leva_tetto
classe = "movimento" if abs(pct) > 2.0 * max_mkt else "ambiguo"
nota = None
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
prima=float(prev["equity"]), dopo=float(cur["equity"]),
delta=delta, pct=float(pct),
mercato_max_pct=(None if max_mkt is None else float(max_mkt)),
classe=classe))
mercato_nota=nota, classe=classe, fonte="rilevato"))
# --- dichiarati dall'operatore: l'importo lo sa solo lui ------------------------------
ts_rows = [pd.Timestamp(r["ts_utc"]) for r in rows]
for d in dichiarati:
if not rows or d["ts"] <= ts_rows[0] or d["ts"] > ts_rows[-1]:
avvisi.append(f"movimento dichiarato {d['ts']:%Y-%m-%dT%H:%M}Z ${d['delta']:+,.2f} "
f"fuori dalle letture [{rows[0]['ts_utc'][:16] if rows else '-'} .. "
f"{rows[-1]['ts_utc'][:16] if rows else '-'}]: non applicato")
continue
i = next(k for k in range(1, len(rows)) if ts_rows[k - 1] < d["ts"] <= ts_rows[k])
prev, cur = rows[i - 1], rows[i]
nota_d = (f"dichiarato dall'operatore"
+ (f" il {d['dichiarato_il']}" if d.get("dichiarato_il") else "")
+ (f": {d['nota']}" if d["nota"] else "")
+ (f" (banda ±${d['banda']:,.2f})" if d["banda"] else ""))
# (#27) l'aggancio "dichiarato+rilevato" vale SOLO contro un evento rilevato: se si
# cercasse un qualunque evento sullo stesso ts_dopo, il secondo dichiarato nello stesso
# intervallo troverebbe il primo (gia' "dichiarato") e lo SOVRASCRIVEREBBE, perdendo
# il primo importo invece di somarli.
rilevato = next((e for e in eventi
if e["ts_dopo"] == cur["ts_utc"] and e["fonte"] == "rilevato"), None)
if rilevato is not None: # rilevato E dichiarato: l'importo e' quello dichiarato
rilevato.update(delta=d["delta"], pct=d["delta"] / float(prev["equity"]) if prev["equity"] else None,
classe="movimento", fonte="dichiarato+rilevato",
dopo=float(prev["equity"]) + d["delta"], dichiarazione=nota_d)
continue
gia = next((e for e in eventi if e["ts_dopo"] == cur["ts_utc"]
and e["fonte"] in ("dichiarato", "dichiarato+rilevato")), None)
if gia is not None: # un altro dichiarato nello stesso intervallo: si SOMMA
nuovo_delta = gia["delta"] + d["delta"]
gia.update(delta=nuovo_delta,
pct=(nuovo_delta / float(gia["prima"]) if gia["prima"] else None),
dopo=float(gia["prima"]) + nuovo_delta,
dichiarazione=(gia["dichiarazione"] + "; " + nota_d))
continue
eventi.append(dict(ts_prima=prev["ts_utc"], ts_dopo=cur["ts_utc"],
prima=float(prev["equity"]), dopo=float(prev["equity"]) + d["delta"],
delta=d["delta"],
pct=(d["delta"] / float(prev["equity"]) if prev["equity"] else None),
mercato_max_pct=None, mercato_nota=None, classe="movimento",
fonte="dichiarato", dichiarazione=nota_d))
eventi.sort(key=lambda e: e["ts_dopo"])
return dict(eventi=eventi,
certi=float(sum(e["delta"] for e in eventi if e["classe"] == "movimento")),
ambigui=float(sum(e["delta"] for e in eventi if e["classe"] == "ambiguo")),
soglia=soglia, leva_tetto=leva_tetto)
soglia=soglia, leva_tetto=leva_tetto, avvisi=avvisi)
def rendimento_twr(con, fino_ts: str) -> dict:
@@ -191,35 +292,61 @@ def rendimento_twr(con, fino_ts: str) -> dict:
apre uno all'equity DOPO; il TWR e' il prodotto dei rendimenti di segmento, meno uno. Gli
eventi `ambiguo` NON spezzano niente (P12: restano nel rendimento, dichiarati accanto).
TRE STATI: `twr` e' un numero, oppure None con `motivo` (serie vuota, base zero).
⚠️ LIMITE DICHIARATO (D5). L'intervallo che CONTIENE un movimento certo esce INTERO dal
rendimento: il suo P&L di mercato (marcatura delle posizioni aperte, funding; in un buco di
lettura — fino a 10h nella serie vera — l'intero buco) finisce in `certi`, quindi anche fuori
da `trading`. E' limitato per costruzione dal margine del classificatore (|salto| >
2·max_mkt·leva·equity), percio' l'errore massimo vale max_mkt·leva·equity: fino a META' del
movimento riconosciuto. Il 25/08: libro a ~$263 lordi, mercato a ±0,2% -> ~$0,5 su $1.399,39.
Non si stima e non si corregge (P12): si dichiara. Un intervallo di sola marcatura fra due
movimenti consecutivi, o prima del primo, ha lunghezza zero e NON produce un segmento.
TRE STATI: `twr` e `trading` sono numeri, oppure None con `motivo`. Mercato NON misurabile
per almeno un salto (feed 1h fermo, o assente) => entrambi None: non si conosce la classe
del salto, quindi non si conosce il rendimento — e `e1/e0-1` non torna sotto un'etichetta
piu' autorevole. Base di equity zero =>
entrambi None: il salto 0 -> X del primo versamento e' INVISIBILE a `movimenti_capitale`
(una base zero non ha percentuale), quindi `certi` lo perderebbe e `trading` varrebbe
l'intero conto — un numero etichettato "al netto dei versamenti" che li contiene tutti.
"""
rows = con.execute("SELECT ts_utc, equity FROM equity WHERE ts_utc <= ? ORDER BY ts_utc",
(fino_ts,)).fetchall()
if not rows:
return dict(twr=None, motivo="nessuna lettura di equity", segmenti=[], eventi=[],
certi=0.0, ambigui=0.0, delta_equity=None, trading=None, letture=0)
mv = movimenti_capitale(con, fino_ts)
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
mv = movimenti_capitale(con, fino_ts, rows=rows) # stessa lettura, un solo istante
e0, e1 = float(rows[0]["equity"]), float(rows[-1]["equity"])
if e0 <= 0:
return dict(twr=None, trading=None,
motivo="base di equity nulla: il primo versamento non e' classificabile",
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
delta_equity=e1 - e0, letture=len(rows))
non_mis = [e for e in mv["eventi"] if e.get("mercato_nota") and e["classe"] != "movimento"]
if non_mis:
# un salto oltre soglia che NON si e' potuto confrontare col mercato non e' ne' un
# movimento ne' un rendimento: il TWR non si stampa (altrimenti `e1/e0-1` tornerebbe
# sotto l'etichetta "TWR", il numero che il debito 14 e' nato per uccidere)
return dict(twr=None, trading=None,
motivo=(f"{len(non_mis)} salto/i non classificabile/i — "
+ non_mis[0]["mercato_nota"]),
segmenti=[], eventi=mv["eventi"], certi=mv["certi"], ambigui=mv["ambigui"],
delta_equity=e1 - e0, letture=len(rows))
certi = [e for e in mv["eventi"] if e["classe"] == "movimento"]
segmenti, da_ts, da_eq = [], rows[0]["ts_utc"], e0
for ev in certi: # gia' in ordine di tempo
if da_eq:
if da_eq and ev["ts_prima"] != da_ts: # lunghezza zero = niente segmento
segmenti.append(dict(da=da_ts, a=ev["ts_prima"], e_da=da_eq, e_a=ev["prima"],
ret=ev["prima"] / da_eq - 1.0))
da_ts, da_eq = ev["ts_dopo"], ev["dopo"]
if da_eq:
if da_eq and rows[-1]["ts_utc"] != da_ts:
segmenti.append(dict(da=da_ts, a=rows[-1]["ts_utc"], e_da=da_eq, e_a=e1,
ret=e1 / da_eq - 1.0))
if not segmenti or e0 <= 0:
return dict(twr=None, motivo="base di equity nulla", segmenti=segmenti, eventi=mv["eventi"],
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
trading=e1 - e0 - mv["certi"], letture=len(rows))
twr = 1.0
for sg in segmenti:
for sg in segmenti: # nessun segmento = nessun tempo a mercato = 0
twr *= 1.0 + sg["ret"]
return dict(twr=twr - 1.0, motivo=None, segmenti=segmenti, eventi=mv["eventi"],
certi=mv["certi"], ambigui=mv["ambigui"], delta_equity=e1 - e0,
trading=e1 - e0 - mv["certi"], letture=len(rows))
trading=e1 - e0 - mv["certi"], letture=len(rows), avvisi=mv.get("avvisi") or [])
def pnl_giorno(con, giorno: date) -> dict:
@@ -240,7 +367,10 @@ def pnl_giorno(con, giorno: date) -> dict:
e_arm = con.execute("SELECT equity FROM equity ORDER BY ts_utc LIMIT 1").fetchone()
delta = (e_fin - e_ini) if (e_ini is not None and e_fin is not None) else None
cum = (e_fin - e_arm["equity"]) if (e_fin is not None and e_arm) else None
mv = movimenti_capitale(con, f"{g}T23:59:59+00:00")
# il cumulato dall'arming, i movimenti e il trading al netto vengono da rendimento_twr:
# UNA funzione per i due lettori (report e giornale), non due aritmetiche (revisione 02/09)
r_twr = rendimento_twr(con, f"{g}T23:59:59+00:00")
mv = dict(eventi=r_twr["eventi"], certi=r_twr["certi"], ambigui=r_twr["ambigui"])
ev_g = [e for e in mv["eventi"] if e["ts_dopo"][:10] == g]
mov_g = sum(e["delta"] for e in ev_g if e["classe"] == "movimento")
amb_g = sum(e["delta"] for e in ev_g if e["classe"] == "ambiguo")
@@ -254,7 +384,11 @@ def pnl_giorno(con, giorno: date) -> dict:
movimenti_giorno=ev_g, mov_giorno=float(mov_g), mov_giorno_ambiguo=float(amb_g),
trading_giorno=(delta - mov_g) if delta is not None else None,
movimenti_arming=mv["eventi"], mov_arming=mv["certi"], mov_arming_ambiguo=mv["ambigui"],
trading_da_arming=(cum - mv["certi"]) if cum is not None else None)
trading_da_arming=(r_twr["trading"] if cum is not None else None),
twr_da_arming=(r_twr["twr"] if cum is not None else None), twr_motivo=r_twr["motivo"])
MINUTO_GIRO = 47 # deve restare d'accordo con la crontab e scripts/cron_book.sh (#35 C9)
def salute(con, giorno: date) -> dict:
@@ -262,11 +396,15 @@ def salute(con, giorno: date) -> dict:
Un giorno in corso confrontato con 24 produce un allarme a ogni esecuzione — e un
allarme che scatta sempre e' un allarme che non verra' letto il giorno che e' vero.
Il giro dell'ora CORRENTE scatta al minuto `MINUTO_GIRO` (il cron e' `47 * * * *`): prima
di quel minuto non e' ancora avvenuto, e contarlo comunque (#35 C9) segnalava un giro
"mancante" che semplicemente non era ancora il suo turno.
"""
lb = stato_libro(con, giorno)
adesso = datetime.now(timezone.utc)
in_corso = giorno == adesso.date()
atteso = (adesso.hour + 1) if in_corso else 24
atteso = (adesso.hour + (1 if adesso.minute >= MINUTO_GIRO + 1 else 0)) if in_corso else 24
fatti = lb.get("giri", 0)
return dict(giri_book=fatti, feed_skh_min=lb.get("feed_skh_min"), atteso_giri=atteso,
giorno_in_corso=in_corso, giri_mancanti=max(0, atteso - fatti))
@@ -362,7 +500,11 @@ def lettura(con, voce: dict) -> list[dict]:
# --- movimenti di capitale: prima del P&L, perche' lo ridefiniscono ----------------------
for e in p.get("movimenti_giorno") or []:
h0, h1 = e["ts_prima"][11:16], e["ts_dopo"][11:16]
if e["classe"] == "movimento":
if e["classe"] == "movimento" and e.get("fonte", "rilevato") != "rilevato":
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
f"{h1} UTC, {e.get('dichiarazione') or 'dichiarato'}. "
"Scorporato dal P&L di trading qui sotto.", "nota")
elif e["classe"] == "movimento":
dire("movimento", f"**Movimento di capitale ${e['delta']:+,.2f}** fra le {h0} e le "
f"{h1} UTC: salto {100*e['pct']:+.1f}% contro un massimo spiegabile "
f"dal mercato di ±{100*e['mercato_max_pct']:.1f}% al tetto di leva. "
@@ -423,17 +565,17 @@ def lettura(con, voce: dict) -> list[dict]:
# --- quanto capitale e' esposto ----------------------------------------------------------
lv = lb.get("leva_lorda")
if lv is not None:
# Il tetto si LEGGE da config/live.json. Ridichiararlo qui e' lo schema che questo
# progetto ha gia' pagato quattro volte (fee_watch, test di guardia sul cap, taratura
# venue_watch, indice del namespace): un sorvegliante che ridichiara il proprio
# bersaglio continua a passare il giorno che il bersaglio cambia.
# Il tetto si LEGGE da `book.leva_tetto`, non ridichiarato qui. Ridichiararlo e' lo schema
# che questo progetto ha gia' pagato quattro volte (fee_watch, test di guardia sul cap,
# taratura venue_watch, indice del namespace) — e una quinta, con una formula diversa da
# quella di `movimenti_capitale` e di `usde.frazione_cuscino`, e' quella corretta da #28.
from src.live import book as _book # lazy: prende il tetto vivo, anche monkeypatchato
try:
cfg = json.loads((PROJECT_ROOT / "config" / "live.json").read_text())
cap_frac = float(cfg["max_notional_per_asset_frac"])
tetto = cap_frac * len(lb["asset"])
tetto, come = _book.leva_tetto(cfg)
dire("leva", f"Leva lorda **{lv:.2f}x** su un tetto di {tetto:.2f}x "
f"(`max_notional_per_asset_frac` {cap_frac:.2f} x {len(lb['asset'])} "
f"asset, letto da `config/live.json`): margine {tetto - lv:.2f}x.")
f"({come}, da `config/live.json` e `book.leva_tetto`): "
f"margine {tetto - lv:.2f}x.")
except Exception as e:
dire("leva", f"Leva lorda **{lv:.2f}x**; tetto non verificabile "
f"({type(e).__name__} su config/live.json).", "attenzione")
@@ -468,16 +610,19 @@ def lettura(con, voce: dict) -> list[dict]:
# il cumulato di TRADING: i movimenti di capitale certi escono, o un versamento
# diventa "concentrazione di P&L" (e' successo: 25/08, +$1.399 letti come profitto)
mv_ev = [e for e in (p.get("movimenti_arming") or []) if e["classe"] == "movimento"]
g7s = (date.fromisoformat(voce["giorno"]) - timedelta(days=7)).isoformat()
tot = e1 - e0 - sum(e["delta"] for e in mv_ev)
picco = max(r["equity"] for r in eq)
g7 = [r for r in eq if r["ts_utc"][:10] >= g7s]
mv7 = sum(e["delta"] for e in mv_ev if e["ts_dopo"][:10] >= g7s)
# (#35 C15) "ultimi 7 giorni" deve essere VERO: la baseline e' l'ULTIMA lettura al o
# prima del giorno-7 a fine giornata — non la PRIMA lettura di quel giorno. Filtrare per
# data (>= giorno-7) include anche le ore del giorno-7 gia' trascorse, cioe' 8 giorni di
# calendario, non 7.
g7_cutoff = (date.fromisoformat(voce["giorno"]) - timedelta(days=7)).isoformat() + "T23:59:59+00:00"
base7 = next((r for r in reversed(eq) if r["ts_utc"] <= g7_cutoff), None)
mv7 = sum(e["delta"] for e in mv_ev if e["ts_dopo"] > g7_cutoff)
# Soglia di rilevanza: su un cumulato di pochi dollari la "concentrazione" e' aritmetica
# vera e informazione zero. Una regola che si accende su $2 insegna a saltare la sezione.
rilevante = abs(tot) >= max(5.0, 0.01 * e1)
if rilevante and g7:
quota = (e1 - g7[0]["equity"] - mv7) / tot
if rilevante and base7 is not None:
quota = (e1 - base7["equity"] - mv7) / tot
if quota > 0.60:
coda = ("Un risultato concentrato in una finestra non e' un tasso di rendimento: "
"e' un evento.")
@@ -486,10 +631,22 @@ def lettura(con, voce: dict) -> list[dict]:
"senza questi 7 giorni il libro sarebbe sotto. " + coda)
dire("concentrazione", f"**Il {100*quota:.0f}% di tutto il P&L cumulato "
f"(${tot:+,.2f}) viene dagli ultimi 7 giorni.** " + coda, "nota")
if picco > 0:
dd = (e1 - picco) / picco
# (#35 C7) il DD si calcola sulla serie AL NETTO dei movimenti CERTI (gli stessi che il
# TWR scorpora): sulla serie grezza un prelievo si legge come drawdown di trading e un
# versamento alza il picco, mascherando un DD vero appena prima del versamento.
mv_ord = sorted(mv_ev, key=lambda e: e["ts_dopo"])
cum_mov, i_mov, eq_netto = 0.0, 0, []
for r in eq:
while i_mov < len(mv_ord) and r["ts_utc"] >= mv_ord[i_mov]["ts_dopo"]:
cum_mov += mv_ord[i_mov]["delta"]
i_mov += 1
eq_netto.append(r["equity"] - cum_mov)
picco_netto = max(eq_netto)
if picco_netto > 0:
dd = (eq_netto[-1] - picco_netto) / picco_netto
if dd < -0.005:
dire("drawdown", f"Equity **{100*dd:.2f}%** sotto il picco (${picco:,.2f}). "
dire("drawdown", f"Equity **{100*dd:.2f}%** sotto il picco (${picco_netto:,.2f}, "
"al netto dei movimenti di capitale certi). "
"⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.", "nota")
# --- volatilita' implicita contro realizzata ---------------------------------------------
@@ -626,6 +783,10 @@ def rendi_markdown(con, voce: dict) -> str:
if p.get("mov_arming"):
riga_c += (f" — di cui ${p['mov_arming']:+,.2f} versati/prelevati -> trading "
f"**{_n(p.get('trading_da_arming'),'${:+,.2f}')}**")
if p.get("twr_da_arming") is not None:
riga_c += f" · TWR **{100 * p['twr_da_arming']:+.2f}%**"
elif p.get("twr_motivo"):
riga_c += f" · TWR n/d ({p['twr_motivo']})"
if p.get("mov_arming_ambiguo"):
riga_c += f" — ⚠️ ${p['mov_arming_ambiguo']:+,.2f} ambigui non attribuiti"
L += ["", "## P&L", "",
+82
View File
@@ -49,6 +49,9 @@ import numpy as np
MIN_COVERAGE = 0.80 # sotto: la serie ha buchi -> non ci si decide sopra un gate
MIN_BARS_PER_JUDGE = 2 # con una barra sola non esiste ne' coda ne' copertura
GRAZIA_PREMATURO_MIN = 5.0 # tolleranza fra chiusura della barra e mtime del file (clock/IO)
SOGLIA_ORARI_H = 6.0 # (#30) max_age_h <= questa soglia = "sorvegliante orario": i tre
# con max_age 3h (cuscino_watch, venue_watch, collect_chain) oggi,
# derivato dalla tabella e non da una lista di nomi cablata
@dataclass(frozen=True)
@@ -92,6 +95,42 @@ MONITORS: tuple[MonitorSpec, ...] = (
MonitorSpec("usde_watch", "live", "usde_watch.jsonl", 24.0, max_age_h=30.0,
gate="allarmi depeg/quota USDE + verdetto eligibilita' 29/08",
open_labeled=False),
# Cuscino USDC di regolamento (cron :53, ORARIO, debito §5.17 / issue #3): se si ferma nessuno
# vede il saldo USDC scendere sotto il cuscino, e la riconversione automatica non parte.
# max_age 3h = due giri persi; timbri di esecuzione -> open_labeled=False.
MonitorSpec("cuscino_watch", "live", "cuscino_watch.jsonl", 1.0, max_age_h=3.0,
gate="cuscino USDC di regolamento: allerta + riconversione automatica",
open_labeled=False),
# Trigger di regime CC01 (cron_daily 00:30, GIORNALIERO, #12): dal 03/09 al 16/09 e' morto 14
# notti di fila senza che nessuno lo vedesse (#10), perche' non scriveva niente da sorvegliare.
# Il percorso e' quello di `cc01_regime_watch.RUNS` (un test li incrocia, P1). max_age 30h =
# un giro mancato + margine; timbri di esecuzione -> open_labeled=False.
MonitorSpec("cc01_regime_watch", "cc01_regime", "runs.jsonl", 24.0, max_age_h=30.0,
gate="trigger di regime CC01: WARN>=10% / ALERT>=15% funding 30g (riapre il dossier)",
open_labeled=False),
# I sei sorveglianti che generalizzano la riparazione di cc01 (#16): ognuno registra il
# proprio giro in `data/<dir>/runs.jsonl` (src.live.giri.registra_giro), timbri di
# ESECUZIONE come cc01 -> open_labeled=False. cron_daily.sh (GIORNALIERO) per i primi
# cinque: max_age 30h = un giro mancato + margine.
MonitorSpec("edge_watch", "edge_watch", "runs.jsonl", 24.0, max_age_h=30.0,
gate="kill del libro live (Sharpe 36m < -0,5 / protezione TP01)",
open_labeled=False),
MonitorSpec("fee_watch", "fee_watch", "runs.jsonl", 24.0, max_age_h=30.0,
gate="regola fee §3 decisa in anticipo",
open_labeled=False),
MonitorSpec("scale_watch", "scale_watch", "runs.jsonl", 24.0, max_age_h=30.0,
gate="GATE SCALA-01 criterio A2",
open_labeled=False),
MonitorSpec("vrp_f_watch", "vrp_f_watch", "runs.jsonl", 24.0, max_age_h=30.0,
gate="criteri (a)(b) §5.18",
open_labeled=False),
MonitorSpec("venue_news", "venue_news", "runs.jsonl", 24.0, max_age_h=30.0,
gate="annunci Deribit, debito 8",
open_labeled=False),
# cron_book.sh: gira OGNI ORA accanto al libro vivo. max_age 3h = due giri persi.
MonitorSpec("venue_watch", "venue_watch", "runs.jsonl", 1.0, max_age_h=3.0,
gate="tripwire di venue orario",
open_labeled=False),
)
@@ -208,3 +247,46 @@ def alerts_from(rows: list[dict]) -> list[str]:
msg += f" | METTE A RISCHIO il gate {r['gate']}"
out.append(msg)
return out
# ===========================================================================
# GIRO ORARIO (#30). Il giro giornaliero (cron_daily) resta com'e': report completo, avviso
# sempre. I sorveglianti a max_age <= 6h (oggi cuscino_watch, venue_watch, collect_chain) si
# scoprono fermi fino a 24h dopo se nessuno li guarda fuori da quel giro: qui si guardano ogni
# ora, ma si notifica solo cio' che e' NUOVO rispetto all'ultimo avviso (niente Telegram a
# raffica sullo stesso guasto fermo). Tutto PURO: il marcatore lo legge/scrive lo script.
# ===========================================================================
def monitors_orari(monitors: tuple[MonitorSpec, ...] = MONITORS,
soglia_h: float = SOGLIA_ORARI_H) -> tuple[MonitorSpec, ...]:
"""I monitor con cadenza di controllo ORARIA: max_age_h <= soglia_h. Derivato dalla tabella
`MONITORS`, non da una lista di nomi cablata: aggiungere un monitor orario lo include qui
senza toccare questa funzione."""
return tuple(m for m in monitors if m.max_age_h <= soglia_h)
def chiave_alert(row: dict) -> str:
"""Chiave stabile di un alert per il marcatore 'gia' detto': nome + stato, NON il testo
(`why` contiene l'eta' dell'ultima barra, che cambia a ogni giro anche se il guasto e' lo
stesso)."""
return f"{row['name']}:{row['status']}"
def alert_nuovi(rows: list[dict], detti: set[str]) -> list[dict]:
"""Le righe-alert (non OK, non NUOVO — vedi `alerts_from`) la cui chiave NON e' nel
marcatore `detti`: quelle davvero nuove, le sole da notificare."""
return [r for r in rows if r["status"] not in ("OK", "NUOVO") and chiave_alert(r) not in detti]
def marcatore_aggiornato(rows: list[dict], detti: set[str]) -> set[str]:
"""Il marcatore dopo questo giro: le chiavi dei monitor ancora in alert restano/si aggiungono,
quelle dei monitor tornati OK si TOLGONO — cosi' un nuovo guasto sullo stesso monitor riparte
con un nuovo avviso invece di restare 'gia' detto' per sempre. Non scrive nulla: lo fa lo
script, e solo se la notifica e' partita (stessa regola di fee_watch #29)."""
out = set(detti)
for r in rows:
chiavi_monitor = {k for k in out if k.startswith(f"{r['name']}:")}
if r["status"] == "OK":
out -= chiavi_monitor
elif r["status"] != "NUOVO":
out.add(chiave_alert(r))
return out
+8 -2
View File
@@ -87,9 +87,15 @@ def notify(title: str, data: dict | None = None, tentativi: int = 1) -> bool:
ver = f" <code>v{APP_VERSION}</code>"
except Exception:
ver = ""
lines = [f"<b>{title}</b>{ver}"]
# Telegram e' in parse_mode=HTML: un '<' o '&' nudo in un valore fa fallire l'INTERO invio
# (HTTP 400, "can't parse entities"). Successo il 2026-09-10 13:53Z: la prima allerta di
# cuscino_watch conteneva «(< 10% del cuscino)» ed e' andata persa. L'escape sta QUI, nel sink,
# perche' nessuno dei 45 chiamanti passa tag di proposito (verificato con grep) e un chiamante
# nuovo non deve ricordarsene. `send()` resta grezzo per chi costruisce il proprio HTML.
import html
lines = [f"<b>{html.escape(str(title), quote=False)}</b>{ver}"]
for k, v in (data or {}).items():
lines.append(f" {k}: {v}")
lines.append(f" {html.escape(str(k), quote=False)}: {html.escape(str(v), quote=False)}")
return send("\n".join(lines), tentativi=tentativi)
+378
View File
@@ -0,0 +1,378 @@
"""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 # il resto delle voci e' molto piu' piccolo
# #37 C12. CLAUDE.md e' arrivato a 89.590 caratteri (99,5% di MAX_CHARS_VOCE, 27/09): al primo
# paragrafo aggiunto si sarebbe troncato. Tetto SUO, piu' largo — il vincolo vero resta
# MAX_CHARS_TOTALE, applicato A PARTE in `materiale()` a prescindere dal tetto di ogni voce: la
# somma dei tetti per-voce (che supera 400k) non e' il limite, lo e' il secondo giro di `_tronca`.
MAX_CHARS_CLAUDE_MD = 150_000
MAX_CHARS_TOTALE = 400_000
SOGLIA_AVVISO_TETTO = 0.95 # avviso quando una voce supera questa frazione del SUO tetto
_MARCA_VICINO_TETTO = "[… vicino al tetto:" # marcatore condiviso: materiale() lo scrive, i
# consumatori (prompt, rapporto) lo rileggono
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 _cap_voce(nome: str) -> int:
"""Tetto PER-VOCE: CLAUDE.md ne ha uno suo, piu' largo (#37 C12). Il totale
(MAX_CHARS_TOTALE) resta un vincolo A PARTE, applicato in `materiale()` a valle di questo."""
return MAX_CHARS_CLAUDE_MD if nome.startswith("CLAUDE.md") else MAX_CHARS_VOCE
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:
cap = _cap_voce(nome)
# #37 C12: avviso PRIMA di troncare, quando la voce e' vicina al SUO tetto (non ancora al
# tetto totale, sotto): la nota entra nel testo, cosi' arriva sia al prompt del revisore
# sia — via _MARCA_VICINO_TETTO — al rapporto, come [TRONCATO] oggi. Il margine
# (`+ len(nota) <= cap`) e' voluto: la nota stessa non deve MAI far scattare il
# troncamento che sta segnalando in anticipo.
if len(testo) >= SOGLIA_AVVISO_TETTO * cap:
nota = (f"\n{_MARCA_VICINO_TETTO} {len(testo):,} di {cap:,} caratteri "
f"({100 * len(testo) / cap:.0f}%)]")
if len(testo) + len(nota) <= cap:
testo = testo + nota
t, tr = _tronca(testo, cap)
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:
tag = " [TRONCATO]" if tr else (" [VICINO AL TETTO]" if _MARCA_VICINO_TETTO in testo else "")
parti.append(f"\n=== {nome}{tag} ===\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):,} | "
f"{'si' if tr else ('quasi' if _MARCA_VICINO_TETTO in t 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)"
tagliato = ""
# (#35 C13) il taglio va sul testo GREZZO, PRIMA di html.escape: tagliare dopo puo' spezzare
# un'entita' a meta' (es. "&am") e Telegram rifiuta l'intero messaggio ("can't parse
# entities") invece di accorciarlo. Il marcatore di taglio si aggiunge dopo, fuori dall'escape
# (e' testo nostro, senza `&`/`<`: non ha bisogno di escape).
if len(sintesi) > LIMITE_TELEGRAM:
sintesi, tagliato = sintesi[:LIMITE_TELEGRAM], "\n[… sintesi tagliata: il resto e' nel file]"
corpo = html.escape(sintesi, quote=False)
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:
tag = " [troncata]" if tr else (" [vicino al tetto]" if _MARCA_VICINO_TETTO in t else "")
print(f" {len(t):>8,} {n}{tag}")
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
+20 -7
View File
@@ -95,15 +95,20 @@ def _positions(client):
USDE_INDEX_URL = _usde.index_url()
def _collaterale_usde(client) -> tuple[float, str]:
def _collaterale_usde(client) -> tuple[float | None, str]:
"""Valore USD dell'USDE tenuto come collaterale. Il PERCHE' e le proprieta' (depeg che
PASSA nel sizing, clamp sopra la pari, fallback 1.0 dichiarato mai 0) stanno in
src/live/usde.py — difetto d'origine 2026-08-26: `_equity` leggeva solo il conto USDC,
-24% fantasma dopo la prima conversione."""
-24% fantasma dopo la prima conversione.
⚠️ (#20) Un'eccezione sulla LETTURA del conto USDE non e' "0 USDE": e' "non lo so". Ritorna
None (non 0.0) + la nota dell'eccezione -> `_equity` propaga l'ignoto invece di dedurre un
equity piu' basso di quello vero. Eq letta con successo e <= 0 resta 0.0 (conto senza USDE:
quello e' un fatto accertato, legittimo)."""
try:
eq = float(client.account_summary("USDE").get("equity") or 0)
except Exception:
return 0.0, ""
except Exception as e:
return None, f"USDE illeggibile: {type(e).__name__}: {e}"
if eq <= 0:
return 0.0, ""
px, _fonte = _usde.prezzo(client, url=USDE_INDEX_URL)
@@ -117,6 +122,8 @@ def _equity(client, marks):
eq = float(client.account_summary("USDC").get("equity") or 0)
if eq > 1:
usde_usd, usde_note = _collaterale_usde(client)
if usde_usd is None: # (#20) USDE illeggibile -> l'equity e' IGNOTA,
return None, usde_note # non "USDC senza USDE": non si assume 0.
if usde_usd > 0:
return eq + usde_usd, f"mainnet USDC + {usde_note}"
return eq, "mainnet USDC"
@@ -133,7 +140,8 @@ def _equity(client, marks):
if any_ok and tot > 1:
return tot, "mainnet coin-margined"
# real_eq=None qui -> shadow_report ripiega su paper_cap e alza eq_fallback (diagnostica B):
# segnalato a book_execute (alert), non blocca (l'hard-cap $300/asset limita il downside).
# segnalato a book_execute, che in modo SOLO PROTEZIONE non manda alcun rebalance_signed —
# controlla comunque il disaster-SL, che dipende da posizione/mark, non da equity (#21).
return None, "conto flat / non finanziato"
@@ -158,7 +166,9 @@ def shadow_report(offline: bool = False, equity_override: float | None = None) -
eq_basis = ("override" if equity_override is not None
else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)")
# Opzione B (diagnostica): ONLINE ma equity reale non leggibile -> sizing su paper_cap invece del
# conto reale. NON blocca (l'hard-cap $300/asset limita il downside), ma va SEGNALATO.
# conto reale. SEGNALATO, e book_execute passa a SOLO PROTEZIONE: nessun rebalance_signed (non
# si dimensiona un ordine senza sapere quanto vale il conto), ma il disaster-SL si controlla
# comunque per ogni asset (#21).
eq_fallback = (f"equity reale non leggibile ({eq_src}) -> sizing su paper_cap ${paper_cap:,.0f} "
f"invece del conto reale") if (equity_override is None and client is not None
and not real_eq) else None
@@ -208,7 +218,10 @@ def shadow_report(offline: bool = False, equity_override: float | None = None) -
return dict(
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
online=(client is not None and marks_src.get("BTC") == "mainnet"),
# (#25) TUTTI i mark devono venire da mainnet, non solo BTC: con due asset, un solo
# controllo lasciava ETH libero di essere in fallback (close certificata) senza che
# `online` lo dicesse -> il gate offline di book_execute non si accorgeva del guasto.
online=(client is not None and all(marks_src.get(a) == "mainnet" for a in ASSETS)),
real_equity=real_eq, equity=equity, eq_basis=eq_basis,
pos_src=pos_src, pos_error=pos_error, eq_fallback=eq_fallback,
assets=assets, orders=orders, live_trades=live_trades,
+152 -34
View File
@@ -2,20 +2,34 @@
PERCHE' ESISTE
--------------
`data/live/book_executions.jsonl` registra i fill, ma il suo `ts_utc` e' la data della
**barra di segnale** (`book_execute.py`: `pd.Timestamp(r['last_data'])`), non l'ora del fill:
19 righe su 19 a `00:00:00`. L'ora vera sta solo in `logs/cron_book.log` — che e' **gitignored,
non nel backup e ruotabile**. Quindi oggi la cronologia reale del libro live vive in un file
che una rotazione cancella, e nessuno se ne accorgerebbe.
`logs/cron_book.log` e' **gitignored, non nel backup e ruotabile**, ed e' la fonte che `sync()`
usa per popolare il DB: una rotazione lo cancellerebbe senza che nessuno se ne accorga.
Questo modulo MATERIALIZZA quella cronologia in `data/live/trades.db` (sqlite), che sta dentro
il perimetro gia' coperto dal backup rotativo della VPS.
⚠️ CORREZIONE (#37 C11, #35 A9). Fino al 2026-08-23 `data/live/book_executions.jsonl` scriveva in
`ts_utc` la data della **barra di segnale** (`pd.Timestamp(r['last_data'])`), non l'ora del fill:
19 righe su 19 a `00:00:00`. Dal 2026-08-24 01:07Z `book_execute.py` (`_registra_fill`) scrive
l'ora VERA del fill (`datetime.now(timezone.utc)` al momento dell'ordine, non piu' la barra),
UNA per fill — con il prezzo a piena precisione (non arrotondato come nella stampa del log) e il
campo `pos_after` (letto dal reconcile subito dopo il rebalance, #39 — None se quella lettura
solleva, o se il passo e' un `RebalanceParziale` a meta'). E' quindi, dal 24/08 in poi, la fonte
con l'ora PIU' precisa per-fill. Dal 2026-09-27 (#40) `sync()` la usa anche per ARRICCHIRE il
DB: `arricchisci_da_jsonl()` gira dopo l'inserimento e prima dei round-trip, e le righe dal 24/08
finiscono con `ts_utc`/`price` dal fill vero e `ts_source='jsonl'` invece che `'cron_log'` (e
`pos_after` quando il jsonl lo porta) — SOLO se l'abbinamento e' univoco, altrimenti resta com'e'
e conta come ambiguo.
FONTI, in ordine di autorita' — e nessuna delle tre e' completa da sola
----------------------------------------------------------------------
1. `logs/cron_book.log` -> ORA VERA del giro + contesto (equity, tp_frac, skh, target, posizione)
e la riga di fill `-> BUY 0.0008 @ $78,094.2 fee 0.02187 (OK)`.
2. `data/live/book_executions.jsonl` -> gli stessi fill con piu' cifre, ma senza ora.
1. `logs/cron_book.log` -> l'ora di INIZIO GIRO (una per run, condivisa da tutti i fill di quel
run, non per-fill) + contesto (equity, tp_frac, skh, target,
posizione) e la riga di fill
`-> BUY 0.0008 @ $78,094.2 fee 0.02187 (OK)`. FONTE usata da `sync()`.
2. `data/live/book_executions.jsonl` -> gli stessi fill, con l'ora VERA per-fill dal 2026-08-24
01:07Z (prima: la data della barra di segnale, 00:00:00), prezzo a
piena precisione e `pos_after`. Entra SOLO in `reconcile()`.
3. venue (`DeribitRead.trade_history`) -> autorevole su `order_id` e timestamp in ms,
**ma tronca**: al 2026-08-23 ritorna 1 trade su BTC e 0 su ETH.
@@ -32,7 +46,7 @@ import json
import re
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
@@ -42,6 +56,10 @@ EXEC_JSONL = PROJECT_ROOT / "data" / "live" / "book_executions.jsonl"
# Data di armamento dell'esecuzione reale (CLAUDE.md, 2026-06-20).
ARMING = "2026-06-20"
# Da qui in poi book_executions.jsonl porta l'ora VERA del fill in `ts_utc` (fix #35 A9, docstring
# sopra): soglia per il controllo di conteggio jsonl-vs-DB in scripts/live/trades_db.py:sync()
# (#37 C11). Prima di questo istante il campo e' la barra di segnale, non comparabile.
CUTOFF_TS_VERO = "2026-08-24T01:07:00+00:00"
SCHEMA = """
CREATE TABLE IF NOT EXISTS fills (
@@ -262,31 +280,43 @@ def reconcile(fills_log: list[Fill], righe_jsonl: list[dict], tol_px: float = 0.
def fifo_roundtrips(fills: list[Fill]) -> tuple[list[dict], dict]:
"""Round-trip chiusi per FIFO + lotti residui. Le fee si allocano PRO-QUOTA sulla qty."""
"""Round-trip chiusi per FIFO + lotti residui. Le fee si allocano PRO-QUOTA sulla qty.
Lotti CON SEGNO (#32 C2): ogni lotto e' long (+1) o short (-1). Un fill chiude PRIMA i
lotti di segno OPPOSTO (FIFO), e l'eccedenza — se resta qty dopo aver chiuso tutto il
lato opposto — apre (o accoda) un lotto nel segno del fill stesso. Prima di questa
riparazione un SELL senza lotti long da chiudere spariva in silenzio (nessun lotto,
nessun round-trip): l'inversione long -> short in un solo fill e' lo stesso meccanismo,
solo che il lato opposto si esaurisce a meta' fill invece che all'inizio.
"""
from collections import deque, defaultdict
lotti: dict[str, deque] = defaultdict(deque)
lotti: dict[str, deque] = defaultdict(deque) # ciascun lotto: [qty, price, ts, fee_u, segno]
rts: list[dict] = []
for f in sorted(fills, key=lambda x: (x.ts_utc, x.asset)):
fee_u = f.fee / f.qty if f.qty else 0.0
if f.side == "buy":
lotti[f.asset].append([f.qty, f.price, f.ts_utc, fee_u])
else:
resto = f.qty
while resto > 1e-12 and lotti[f.asset]:
lq, lp, lts, lfee_u = lotti[f.asset][0]
usa = min(resto, lq)
lordo = usa * (f.price - lp)
quota_fee = usa * (lfee_u + fee_u)
ore = (datetime.fromisoformat(f.ts_utc) - datetime.fromisoformat(lts)).total_seconds() / 3600
rts.append(dict(asset=f.asset, qty=usa, ts_in=lts, px_in=lp, ts_out=f.ts_utc,
px_out=f.price, ore_tenuta=ore, pnl_lordo=lordo,
fee_quota=quota_fee, pnl_netto=lordo - quota_fee))
lq -= usa; resto -= usa
if lq <= 1e-12:
lotti[f.asset].popleft()
else:
lotti[f.asset][0][0] = lq
aperti = {a: [dict(qty=q, price=p, ts=t) for q, p, t, _ in dq] for a, dq in lotti.items() if dq}
segno_fill = 1 if f.side == "buy" else -1
dq = lotti[f.asset]
resto = f.qty
while resto > 1e-12 and dq and dq[0][4] == -segno_fill:
lq, lp, lts, lfee_u, lseg = dq[0]
usa = min(resto, lq)
# long chiuso da una vendita: (exit - entry); short chiuso da un acquisto: (entry - exit)
lordo = usa * (f.price - lp) if lseg == 1 else usa * (lp - f.price)
quota_fee = usa * (lfee_u + fee_u)
ore = (datetime.fromisoformat(f.ts_utc) - datetime.fromisoformat(lts)).total_seconds() / 3600
rts.append(dict(asset=f.asset, qty=usa, ts_in=lts, px_in=lp, ts_out=f.ts_utc,
px_out=f.price, ore_tenuta=ore, pnl_lordo=lordo,
fee_quota=quota_fee, pnl_netto=lordo - quota_fee,
direzione=("long" if lseg == 1 else "short")))
lq -= usa; resto -= usa
if lq <= 1e-12:
dq.popleft()
else:
dq[0][0] = lq
if resto > 1e-12: # l'eccedenza apre/accoda nel segno del fill
dq.append([resto, f.price, f.ts_utc, fee_u, segno_fill])
aperti = {a: [dict(qty=q, price=p, ts=t, direzione=("long" if s == 1 else "short"))
for q, p, t, _, s in dq] for a, dq in lotti.items() if dq}
return rts, aperti
@@ -321,6 +351,86 @@ def upsert_fills(con: sqlite3.Connection, fills: list[Fill]) -> int:
return n
def _ts_utc(s) -> datetime | None:
"""Parsa un timestamp ISO in un datetime AWARE utc. None se illeggibile.
Le 19 righe scritte prima del fix #35 A9 (barra di segnale, `pd.Timestamp` in stampa: es.
"2026-07-08 00:00:00", spazio invece di 'T', nessun offset) sono NAIVE — senza questa
normalizzazione, confrontarle con un ts_utc del DB (sempre aware, +00:00) solleva
`TypeError: can't compare offset-naive and offset-aware datetimes` (misurato sui dati veri,
27/09/2026). Sono tutte precedenti a CUTOFF_TS_VERO: qui si limita a non farle esplodere,
non a farle abbinare — restano fuori dalla finestra di qualunque candidato dal 24/08."""
if not s:
return None
try:
ts = datetime.fromisoformat(str(s))
except ValueError:
return None
return ts.replace(tzinfo=timezone.utc) if ts.tzinfo is None else ts
def arricchisci_da_jsonl(con: sqlite3.Connection, righe_jsonl: list[dict]) -> tuple[int, int]:
"""Arricchisce le righe `ts_source='cron_log'` con l'ora vera, il prezzo e `pos_after` letti
da `book_executions.jsonl` (#40, TradingBook#11: lo sviluppo lasciato non fatto dal
commento sopra). NON tocca `fill_id`: e' la chiave della riga, calcolata all'INSERT da
ts_utc/asset/side/qty/price ORIGINALI (cron_log) — qui si fa un UPDATE per `fill_id`, che
cambia solo le colonne, mai la chiave con cui la riga e' indicizzata.
Candidate: `ts_source='cron_log'` e `ts_utc >= CUTOFF_TS_VERO` (prima di quella soglia il
jsonl portava la barra di segnale, non l'ora vera — non ha una controparte comparabile).
Abbinamento a una riga jsonl: stesso asset, stesso side, |qty - filled| < 1e-9, e ts jsonl in
[ts_utc DB, ts_utc DB + 15 minuti] (il DB ha l'ora di INIZIO GIRO, il jsonl l'ora del fill,
che viene dopo). Esattamente UN candidato -> UPDATE; zero o piu' di uno -> non si tocca (non
si indovina quale, o se ce n'e' uno) e conta come ambiguo. Un record jsonl assegnato non si
riusa: due fill identici nello stesso giro non finiscono abbinati alla stessa riga.
Idempotente: una riga gia' `ts_source='jsonl'` non e' piu' candidata al giro successivo.
Ritorna (righe_aggiornate, righe_ambigue)."""
candidati = con.execute(
"SELECT fill_id, ts_utc, asset, side, qty FROM fills "
"WHERE ts_source='cron_log' AND ts_utc >= ? ORDER BY ts_utc, asset",
(CUTOFF_TS_VERO,)).fetchall()
usati: set[int] = set() # indici in righe_jsonl gia' assegnati a un fill_id
aggiornate = 0
ambigue = 0
for riga in candidati:
ts_db = _ts_utc(riga["ts_utc"])
if ts_db is None:
ambigue += 1 # ts illeggibile: non e' un abbinamento, e' un dato rotto
continue
fine = ts_db + timedelta(minutes=15)
trovati = []
for i, r in enumerate(righe_jsonl):
if i in usati:
continue
if r.get("asset") != riga["asset"] or r.get("side") != riga["side"]:
continue
filled = r.get("filled")
if filled is None or abs(float(filled) - riga["qty"]) >= 1e-9:
continue
ts_j = _ts_utc(r.get("ts_utc"))
if ts_j is None or not (ts_db <= ts_j <= fine):
continue
trovati.append(i)
if len(trovati) != 1:
ambigue += 1
continue
i = trovati[0]
r = righe_jsonl[i]
usati.add(i)
pos_after = r.get("pos_after")
if pos_after is None:
con.execute("UPDATE fills SET ts_utc=?, ts_source='jsonl', price=? WHERE fill_id=?",
(r["ts_utc"], r["price"], riga["fill_id"]))
else:
con.execute(
"UPDATE fills SET ts_utc=?, ts_source='jsonl', price=?, pos_after=? WHERE fill_id=?",
(r["ts_utc"], r["price"], pos_after, riga["fill_id"]))
aggiornate += 1
con.commit()
return aggiornate, ambigue
def upsert_equity(con: sqlite3.Connection, punti: list[tuple[str, float, str]]) -> int:
n = 0
for ts, v, src in punti:
@@ -332,8 +442,12 @@ def upsert_equity(con: sqlite3.Connection, punti: list[tuple[str, float, str]])
def rebuild_roundtrips(con: sqlite3.Connection) -> int:
"""Ricalcola da zero: i round-trip sono DERIVATI, mai inseriti a mano."""
righe = con.execute("SELECT * FROM fills ORDER BY ts_utc, asset").fetchall()
"""Ricalcola da zero: i round-trip sono DERIVATI, mai inseriti a mano.
`qty > 0` (#32 C10): un fill "NON VERIFICATO" da `place_order error` puo' arrivare a
qty 0 — non e' un trade, resta nel DB marcato (`verified=0`) ma fuori dal FIFO.
"""
righe = con.execute("SELECT * FROM fills WHERE qty > 0 ORDER BY ts_utc, asset").fetchall()
fills = [Fill(ts_utc=r["ts_utc"], asset=r["asset"], side=r["side"], qty=r["qty"],
price=r["price"], fee=r["fee"]) for r in righe]
rts, _ = fifo_roundtrips(fills)
@@ -358,7 +472,8 @@ def get_meta(con: sqlite3.Connection, k: str, default=None):
def stato_aperto(con: sqlite3.Connection) -> dict:
righe = con.execute("SELECT * FROM fills ORDER BY ts_utc, asset").fetchall()
# qty > 0 (#32 C10): stessa esclusione di rebuild_roundtrips, un fill a qty 0 non e' posizione.
righe = con.execute("SELECT * FROM fills WHERE qty > 0 ORDER BY ts_utc, asset").fetchall()
fills = [Fill(ts_utc=r["ts_utc"], asset=r["asset"], side=r["side"], qty=r["qty"],
price=r["price"], fee=r["fee"]) for r in righe]
_, aperti = fifo_roundtrips(fills)
@@ -366,7 +481,10 @@ def stato_aperto(con: sqlite3.Connection) -> dict:
for a, lotti in aperti.items():
q = sum(x["qty"] for x in lotti)
costo = sum(x["qty"] * x["price"] for x in lotti)
out[a] = dict(qty=q, prezzo_medio=costo / q if q else 0.0, dal=min(x["ts"] for x in lotti))
# un asset ha un solo segno di lotti aperti alla volta (fifo_roundtrips chiude prima
# il lato opposto): la direzione del primo lotto vale per tutti (#32 C2).
out[a] = dict(qty=q, prezzo_medio=costo / q if q else 0.0, dal=min(x["ts"] for x in lotti),
direzione=lotti[0]["direzione"])
return out
+55
View File
@@ -36,6 +36,10 @@ _DEFAULTS = {
"venue_cap_misurato": "2026-08-31",
"depeg_warn": 0.99,
"depeg_crit": 0.95,
# bande del cuscino USDC (frazioni del cuscino richiesto), issue #7 — vedi `_nota_cuscino`
"cuscino_preavviso_frac": 0.10,
"cuscino_margine_frac": 0.20,
"cuscino_riacquisto_frac": 0.40,
}
@@ -93,3 +97,54 @@ def valuta(eq_usde: float, px: float | None) -> tuple[float, str]:
return eq_usde, f"USDE {eq_usde:,.0f} valutato 1.0000 (indice non leggibile)"
p = min(px, 1.0)
return eq_usde * p, f"USDE {eq_usde:,.0f} @ {p:.4f}"
# --- CUSCINO USDC DI REGOLAMENTO ------------------------------------------------------------
# P&L e funding dei perp USDC-lineari si regolano in USDC, non nel collaterale: a quota USDE alta
# il saldo USDC va negativo alla prima perdita del libro e Deribit lo finanzia allo 0,05%/GIORNO
# (KB "Cross collateral specifications": 18,25%/anno, 4,3x la resa USDE). Il cuscino richiesto e'
# il disaster-SL sulla MASSIMA esposizione lorda che il libro puo' prendere:
# book.leva_tetto() x disaster_sl_pct x equity_totale
# tutto letto da config/live.json e src/live/book.py, MAI ridichiarato (P1). Prima del 10/09 la
# formula viveva in scripts/live/usde_convert.py; il sorvegliante cuscino_watch la deriva da qui,
# e usde_convert pure — due lettori, una formula. ⚠️ Fino al #28 la leva lorda era RIDICHIARATA
# qui (N_ASSET fisso x WEIGHT, senza scala): a scala 1,25 dava 0,30 invece di 0,375. Ora la stessa
# `book.leva_tetto` che usa anche il giornale (src/live/journal.py).
def frazione_cuscino() -> tuple[float, str]:
"""PURA sulla config. -> (frazione dell'equity totale che deve restare in USDC, come)."""
cfg = json.loads(CFG_PATH.read_text())
sl = float(cfg["disaster_sl_pct"])
from src.live import book as B # lazy: book importa shadow, che importa questo modulo
leva, come_leva = B.leva_tetto(cfg)
return leva * sl, f"{come_leva} x SL {sl:.0%}"
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
"""Cuscino USDC di REGOLAMENTO in dollari, per un'equity totale data. -> (usd, come)."""
f, come = frazione_cuscino()
return equity_tot * f, f"{come} su ${equity_tot:,.2f}"
def bande_cuscino(c: dict | None = None) -> dict:
"""PURA sulla config. Le tre frazioni del cuscino che governano cuscino_watch, con l'ordine
verificato: 0 < preavviso < margine < riacquisto. Fuori ordine la banda di isteresi non
esiste (vendita e acquisto si inseguirebbero): si solleva, non si indovina."""
c = c or cfg()
b = dict(preavviso=float(c["cuscino_preavviso_frac"]), margine=float(c["cuscino_margine_frac"]),
riacquisto=float(c["cuscino_riacquisto_frac"]))
if not (0.0 < b["preavviso"] < b["margine"] < b["riacquisto"]):
raise ValueError(f"bande del cuscino fuori ordine (serve 0 < preavviso < margine < riacquisto): {b}")
return b
def quota_ripristino(margine_frac: float | None = None) -> float:
"""PURA sulla config. Quota USDE che lascia il cuscino PIU' un margine (frazione del cuscino):
e' il bersaglio UNICO di ogni conversione automatica — vendita quando il cuscino e' scoperto,
acquisto quando lo slack eccede la banda. Il margine serve a non tornare sul bordo alla prima
ora in perdita (P6: la persistenza e' latenza). Default: `cuscino_margine_frac` di config."""
if margine_frac is None:
margine_frac = bande_cuscino()["margine"]
f, _ = frazione_cuscino()
return max(0.0, 1.0 - f * (1.0 + margine_frac))
+33 -22
View File
@@ -28,19 +28,21 @@ col codice 11051, e' manutenzione; se non risponde affatto, e' il venue ma per u
se la sonda stessa non parte, il verdetto e' IGNOTO — **«non vedo» non e' «va tutto bene»** (P5).
La sonda parte SOLO quando qualcosa e' gia' fallito: sul percorso sano non costa niente.
⚠️ **LA FINESTRA NON E' UNA PROVA.** `in_release_window()` dice soltanto che l'orologio e'
compatibile con lo slot di release. Non declassa niente da sola: declassa solo cio' che la SONDA
ha gia' riconosciuto come manutenzione, e solo dentro la durata ANNUNCIATA. Una manutenzione che
sfora resta rumorosa (il 18/08 Deribit annuncio' 15-30 minuti e resto' giu' oltre l'ora), e un
guasto vero di martedi' mattina resta rumoroso: altrimenti si sarebbe costruito il silenzio
proprio nell'ora in cui e' piu' probabile che serva. E' la stessa logica di
`venue_watch.MAINT_GRACE_HOURS` — *declassa dentro la tolleranza, RIALZA oltre* — applicata pero'
a una domanda diversa (**questo giro** e' spiegato?) e quindi alla risoluzione del giro orario,
non delle ore consecutive. Sono due politiche vicine e distinte: non accorparle senza misurare.
REGOLA P9 — *un allarme massimo speso per un evento atteso e' un allarme che non verra' letto il
giorno che e' vero.* E' il motivo per cui `atteso` esiste: declassa la GRAVITA', **non** registra
di meno. L'episodio finisce nel log e nella notifica comunque.
⚠️ **IL RAMO CHE DECLASSAVA E' MORTO, RIMOSSO DA `diagnose()` (#35 B11, 27/09/2026).**
`in_release_window()` diceva se l'orologio cadeva nello slot di release Deribit (martedi'
09:00-09:30 UTC); `diagnose()` la usava per declassare la GRAVITA' di una manutenzione gia'
riconosciuta dalla SONDA (REGOLA P9 — *un allarme massimo speso per un evento atteso e' un
allarme che non verra' letto il giorno che e' vero*). Ma l'UNICO chiamante di `diagnose()` e'
`book_execute.py`, in cron al minuto **:47** di ogni ora (dal 25/08): la finestra 09:00-09:30 non
contiene mai :47, quindi quel ramo non poteva MAI diventare vero — si leggeva come una
declassazione attiva e non lo e' mai stata. Il campo `atteso` RESTA sul verdetto
(`book_execute.py` lo scrive nel dict del report: cambiare il formato per una pulizia e' un costo
che non vale la pena) ma e' hardcoded a False in `diagnose()`. `in_release_window()` e le
costanti `RELEASE_*` restano invece nel modulo: le usa `test_book_cadenza.py`/`test_revisione.py`
per verificare che i cron di book/review evitino lo stesso slot — uno scopo indipendente da
`diagnose()` e ancora valido. Per declassare di nuovo un evento annunciato in `diagnose()` la
strada e' legarlo alla DURATA dell'annuncio (`venue_news`, se e quando esiste), non all'orologio
di un chiamante che non e' mai sincrono con lo slot.
"""
from __future__ import annotations
@@ -53,7 +55,7 @@ import requests
from src.live.venue_watch import is_maintenance # P1: la firma 11051 vive LI', non qui
# --- verdetti -----------------------------------------------------------------------------
V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051: atteso se dentro lo slot
V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051
V_VENUE_GIU = "VENUE_GIU" # il venue non risponde, e non dice manutenzione
V_GATEWAY = "GATEWAY" # il venue sta bene -> il guasto e' NOSTRO (riparabile)
V_IGNOTO = "IGNOTO" # la sonda non e' partita: "non vedo" (P5)
@@ -70,8 +72,12 @@ PROBE_TIMEOUT = 8.0
# 30/06/2026 alle 09:00 UTC, "down for around 15-30 minutes").
# ⚠️ M10: *due punti non fanno una tendenza*. Qui i punti sono quattro e c'e' una meccanica
# dichiarata dal venue, ma resta una REGOLARITA' OSSERVATA, non un contratto: se Deribit sposta
# lo slot, questa costante mente in silenzio. E' per questo che la finestra non decide da sola —
# decide la sonda, e la finestra al massimo declassa.
# lo slot, questa costante mente in silenzio.
# ⚠️ #35 B11 (27/09/2026): questa finestra NON declassa piu' niente in `diagnose()` (vedi sotto):
# l'unico chiamante gira in cron al minuto :47, che non ci cade mai dentro. La funzione e le
# costanti restano perche' `test_book_cadenza.py`/`test_revisione.py` le usano per un controllo
# diverso e ancora valido: che i cron di `book`/`review` cadano FUORI da questo slot (P1, la
# stessa finestra, non una seconda dichiarata a mano).
RELEASE_WEEKDAY = 1 # lunedi'=0 -> 1 = martedi'
RELEASE_START_MIN = 9 * 60 # 09:00 UTC
RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione torna una notizia.
@@ -79,7 +85,8 @@ RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione to
@dataclass(frozen=True)
class Diagnosi:
"""Il verdetto (*cosa*), la spiegazione (*perche'*), e se l'evento era atteso (P9)."""
"""Il verdetto (*cosa*) e la spiegazione (*perche'*). `atteso` resta nel formato ma e' sempre
False (#35 B11): l'unico chiamante non cade mai nella finestra che l'avrebbe declassato."""
verdetto: str
perche: str
atteso: bool
@@ -87,7 +94,8 @@ class Diagnosi:
@property
def gravita(self) -> str:
"""Emoji del titolo di notifica. `atteso` declassa, non silenzia (P9)."""
"""Emoji del titolo di notifica. `atteso` e' sempre False (#35 B11): ramo morto lasciato
per compatibilita' di formato, non per una declassazione che avviene ancora."""
if self.atteso:
return "ℹ️"
return {V_GATEWAY: "🛑", V_VENUE_GIU: "🛑", V_MANUTENZIONE: "⚠️"}.get(self.verdetto, "⚠️")
@@ -106,8 +114,9 @@ class Diagnosi:
def in_release_window(now: datetime | None = None) -> bool:
"""L'orologio e' dentro lo slot di release Deribit (martedi' 09:00-09:30 UTC)?
PURA e testabile. Non e' una prova di niente da sola: vedi la nota in testa al modulo.
"""
PURA e testabile. Non declassa piu' niente in `diagnose()` (#35 B11): resta per chi verifica
che un cron (book, review) sia schedulato fuori da questo slot (P1: la finestra si deriva da
qui, non si riscrive una seconda volta nel test)."""
now = now or datetime.now(timezone.utc)
now = now.astimezone(timezone.utc)
if now.weekday() != RELEASE_WEEKDAY:
@@ -136,7 +145,6 @@ def probe_public(timeout: float = PROBE_TIMEOUT) -> tuple[bool | None, str]:
def diagnose(errori_osservati: list[str] | None = None,
now: datetime | None = None,
sonda=None) -> Diagnosi:
"""Classifica un guasto gia' avvenuto. `sonda` e' iniettabile: i test non toccano la rete.
@@ -169,7 +177,10 @@ def diagnose(errori_osservati: list[str] | None = None,
verdetto = V_VENUE_GIU
perche = "l'API pubblica Deribit non serve, e non dichiara manutenzione"
atteso = verdetto == V_MANUTENZIONE and in_release_window(now)
# #35 B11: qui c'era `verdetto == V_MANUTENZIONE and in_release_window(now)`. L'unico
# chiamante (book_execute.py, cron :47) non cade mai nella finestra 09:00-09:30 di martedi':
# il ramo non poteva mai diventare vero. Il campo resta (formato del report), sempre False.
atteso = False
prova = "; ".join([*errori[:3], grezzo])[:500]
return Diagnosi(verdetto=verdetto, perche=perche, atteso=atteso, prova=prova)
+5
View File
@@ -346,6 +346,11 @@ def run_once(state_path: Path = STATE_PATH, sender=None) -> dict:
f"({o['n_refs']} referenze, spread {o['ref_spread_bps']})")
if report["levels"] and any(l == "ALERT" for l in report["levels"].values()):
report["severity"] = "alert"
# issue #31: senza ALERT, un asset BLIND lasciava severity=None — il titolo del runner
# cadeva sul default piu' forte ("PRIMO PASSO: prelievo di prova") per un episodio che e'
# "non vedo", non "stress misurato". BLIND e' lo stato MORBIDO: cede il passo ad ALERT.
elif report["levels"] and any(l == "BLIND" for l in report["levels"].values()):
report["severity"] = "blind"
# --- blocco piattaforma: adesso passa dalla stessa disciplina degli altri stati
lock_new, lock_lvl = lock_step(st.lock, obs["platform_locked"], obs.get("maintenance", False))
+9 -3
View File
@@ -400,8 +400,14 @@ def deribit_book_sleeves() -> list[Sleeve]:
e VRP01 (opzioni modellate). FULL Sharpe ~1.78 / HOLD ~1.17 / DD ~9% (research; SKH01 daily-step).
Pensato per il deploy reale a basso capitale: stesso conto, stesso feed, ribilanciamento
periodico (vedi src.portfolio.portfolio.rebalance_sim + scripts/portfolio/run_deribit_book.py).
TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione)."""
TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione).
I pesi si DERIVANO da src.live.book (P1): due letterali qui erano una seconda dichiarazione,
libera di divergere dal book vero (#35 B12). Import locale: book importa questo modulo
(_skyhook_positions) a livello di modulo -> un import in testa qui creerebbe un ciclo (come
fa usde.py con book)."""
from src.live import book as B # lazy: vedi nota sul ciclo qui sopra
return [
tp01_sleeve(weight=0.75),
skyhook_sleeve(weight=0.25),
tp01_sleeve(weight=B.W_TP01),
skyhook_sleeve(weight=B.W_SKH),
]
+5
View File
@@ -122,6 +122,11 @@ def _isola_file_operativi_vivi(tmp_path, monkeypatch):
# non deve poter sporcare il working tree (P16: il cron gira dal working tree).
import src.live.journal as journal
monkeypatch.setattr(journal, "JOURNAL_DIR", tmp_path / "journal")
# --- data/live/movimenti_dichiarati.jsonl (2026-09-06) ------------------------------------
# `journal.movimenti_capitale` LEGGE i movimenti dichiarati dall'operatore: se un test
# vedesse quelli veri, un versamento reale entrerebbe in una serie di fixture e il test
# passerebbe o fallirebbe per un motivo che non e' suo. Qui il file non esiste.
monkeypatch.setattr(journal, "MOVIMENTI_DICHIARATI", tmp_path / "movimenti_dichiarati.jsonl")
yield
+34
View File
@@ -0,0 +1,34 @@
"""Utilita' condivise dei test. `tests/` e' su sys.path (c'e' un conftest), quindi si importa
con `from helpers import carica_script`.
PERCHE' ESISTE (2026-09-02). `scripts/` non e' un package importabile per nome, quindi ogni test
che tocca uno script si caricava il modulo a mano con `importlib.util.spec_from_file_location`:
**15 copie della stessa dozzina di righe**, con nomi di modulo diversi per lo stesso file e
differenze silenziose (chi registra in `sys.modules` e chi no — e chi non lo fa rompe `pickle`,
`dataclasses` e i decoratori che risolvono per nome). Una funzione sola, con la registrazione
fatta sempre.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def carica_script(rel: str, nome: str | None = None):
"""Carica per PERCORSO uno script del repo (es. "scripts/live/trades_db.py").
`nome` e' il nome sotto cui il modulo viene registrato in `sys.modules`; il default e' il
nome del file. Registrare PRIMA di eseguire e' voluto: un modulo che si auto-importa o usa
`dataclasses` durante l'esecuzione deve trovarsi.
"""
p = (ROOT / rel).resolve()
assert p.exists(), f"script inesistente: {rel}"
nome = nome or p.stem
spec = importlib.util.spec_from_file_location(nome, p)
mod = importlib.util.module_from_spec(spec)
sys.modules[nome] = mod
spec.loader.exec_module(mod)
return mod
+76
View File
@@ -6,6 +6,7 @@ giorno che non risponde.
"""
from __future__ import annotations
import re
import subprocess
import sys
from datetime import date
@@ -75,6 +76,18 @@ def test_il_prompt_non_contiene_l_analisi_vecchia():
assert "Testo vecchio" not in p and "storico" in p
# ------------------------------------------------- le istruzioni non portano un capitale fisso
def test_istruzioni_non_hanno_un_importo_in_dollari():
"""Il capitale e' l'equity in pagina, non una cifra da ricordare (#13)."""
assert re.search(r"\$\s?\d", A.ISTRUZIONI) is None
def test_istruzioni_nominano_fifo_e_lotto():
"""Piu' round-trip che fill nello stesso giorno e' atteso, non un'anomalia (#13)."""
assert "FIFO" in A.ISTRUZIONI and "lotto" in A.ISTRUZIONI
# ------------------------------------------------- i numeri inventati
def test_scopre_un_numero_che_non_c_e():
@@ -197,6 +210,21 @@ def test_l_agente_non_tocca_la_nota(db, monkeypatch, tmp_path):
assert r["analisi"].startswith("Analisi valida") and r["analisi_stato"] == "ok"
def test_secco_stampa_e_non_salva(db, monkeypatch, tmp_path, capsys):
"""#34: prima del fix `J.salva()` veniva chiamato comunque, prima del controllo su
`secco` — un giro "a secco" scriveva lo stesso le sezioni misurate nel DB."""
import analista as CLI
monkeypatch.setattr(A, "interroga", lambda *a, **k: ("Analisi che non deve finire nel DB.", None))
monkeypatch.setattr(J, "JOURNAL_DIR", tmp_path)
g = date(2026, 8, 21)
assert db.execute("SELECT COUNT(*) c FROM journal").fetchone()["c"] == 0 # DB vuoto in partenza
stato = CLI.scrivi(db, g, modello="finto", secco=True, verbose=False, telegram=False)
assert stato == "ok"
capsys.readouterr() # non e' l'oggetto del test
assert db.execute("SELECT COUNT(*) c FROM journal").fetchone()["c"] == 0 # DB ANCORA vuoto
assert not (tmp_path / f"{g}.md").exists() # scrivi_file non chiamato
def test_se_il_modello_non_risponde_l_analisi_di_IERI_non_resta_in_pagina(db, monkeypatch, tmp_path):
"""Il guasto peggiore: una pagina che mostra l'analisi del giorno prima come se fosse di oggi."""
import analista as CLI
@@ -257,6 +285,39 @@ def test_la_testata_porta_i_numeri_che_si_leggono_dalla_notifica():
t = A.testata(_voce_piena())
assert "$637.36" in t and "+2.36" in t and "+39.30" in t
assert "BTC $+116" in t and "leva 0.41x" in t and "1 fill" in t
# (#35 C8) il cron gira alle 00:30 sul giorno CHIUSO: non e' "oggi", e' "nel giorno".
assert "nel giorno" in t and "oggi" not in t
def test_testata_con_movimento_mostra_trading_giorno_non_il_grezzo():
"""(#35 C8) Un versamento di $1.399 non deve leggersi come P&L: con mov_giorno != 0 e
trading_giorno calcolabile, la cifra mostrata e' trading_giorno, non delta_equity."""
voce = _voce_piena()
voce["pnl"] = dict(voce["pnl"], delta_equity=1401.36, mov_giorno=1399.0, trading_giorno=2.36)
t = A.testata(voce)
assert "+2.36 nel giorno" in t
assert "di cui +1399.00 movimenti" in t
assert "+1401.36" not in t # il grezzo non compare piu' in testata
def test_testata_con_movimento_ma_trading_giorno_none_resta_sul_grezzo():
"""(#35 C8) Se trading_giorno non e' calcolabile (None) si resta su delta_equity, ma con
l'etichetta nuova, non "oggi"."""
voce = _voce_piena()
voce["pnl"] = dict(voce["pnl"], delta_equity=1401.36, mov_giorno=1399.0, trading_giorno=None)
t = A.testata(voce)
assert "+1401.36 nel giorno" in t
assert "di cui" not in t
def test_testata_senza_movimento_mostra_il_grezzo_con_etichetta_nuova():
"""(#35 C8) mov_giorno assente o 0: nessun movimento da scorporare, stesso trattamento di
journal.scorporo (bool())."""
voce = _voce_piena()
voce["pnl"] = dict(voce["pnl"], mov_giorno=0.0, trading_giorno=2.36)
t = A.testata(voce)
assert "+2.36 nel giorno" in t
assert "di cui" not in t
def test_il_messaggio_sfugge_l_HTML_e_converte_il_grassetto():
@@ -284,6 +345,21 @@ def test_il_taglio_a_4096_si_DICHIARA():
assert "tagliato" in m and "docs/journal/2026-08-23.md" in m
def test_taglio_a_cavallo_di_entita_e_tag_resta_HTML_valido():
"""(#35 C13) Il taglio e' sul GREZZO, prima di _b/html.escape e prima di **-><b>: un '&' o
un '**grassetto**' a cavallo del punto di taglio non deve produrre un'entita' spezzata
(es. '&am') ne' un <b> senza chiusura -- Telegram rifiuta l'INTERO messaggio altrimenti
("can't parse entities"). Il pattern si ripete per tutta la lunghezza, cosi' il punto di
taglio (qualunque sia) cade sempre dentro o a cavallo di un '&' o di '**...**'."""
pattern = "parola & altra parola **in grassetto** ancora testo "
analisi = pattern * 200 # ben oltre qualunque budget qui
m = A.per_telegram(_voce_piena(), analisi, "ok", [])
assert "tagliato" in m
assert re.search(r"&(?!amp;|lt;|gt;)", m) is None # nessuna entita' spezzata o '&' nudo
assert m.count("<b>") == m.count("</b>") # ogni <b> aperto e' chiuso
assert m.count("<i>") == m.count("</i>")
def test_un_messaggio_normale_non_viene_toccato():
m = A.per_telegram(_voce_piena(), "Analisi breve.", "ok", [])
assert "tagliato" not in m and m.endswith("Analisi breve.")
+207
View File
@@ -0,0 +1,207 @@
"""La cadenza del libro live e' ORARIA, e le tre dichiarazioni devono restare d'accordo.
Debito §5.7 (chiuso il 2026-09-02): il docstring di `book_execute.py` prescriveva «ogni ~230
minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che gli
scatti del disaster-SL rotolante dipendono dalla cadenza — 1h: BTC 0 / ETH 1 in 7-8 anni; 4h:
BTC 2 (contro 1 del pavimento); 24h: ETH 5 con 2 in 30 giorni. Chi avesse "corretto" il CRON
verso il docstring avrebbe spostato il libro dalla riga 1h a quella 4h.
P1: il bersaglio si DERIVA da cio' che gira. Qui le fonti sono tre — il docstring (cio' che un
umano legge), `cron_book.sh` (cio' che dichiara di essere installato) e la crontab (cio' che
gira davvero) — e il test le confronta fra loro invece di ridichiarare «60 minuti».
"""
from __future__ import annotations
import ast
import re
import shutil
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.live import venue_probe as VP # noqa: E402 lo slot di release, dichiarato li'
from src.strategies.skyhook import LTF_MIN # noqa: E402 la griglia di SKH01, NON ridichiarata
BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py"
CRON_BOOK = ROOT / "scripts" / "cron_book.sh"
RIGA_4H_MIN = 240 # la riga in cui il rotolante di BTC passa da 0 a 2 scatti (r0823_sl_anchor.py);
# la PEGGIORE misurata e' 24h (ETH 5, 2 in 30 giorni), non questa
_RX_CRON5 = re.compile(r"`([\d*/,-]+(?: [\d*/,-]+){4})`")
def cadenza_minuti(expr: str) -> int:
"""Minuti fra due esecuzioni di un'espressione crontab a 5 campi, SOLO per cadenze regolari
(intervalli tutti uguali): `47 * * * *` -> 60, `*/30 * * * *` -> 30, `7 */4 * * *` -> 240.
`*/45` (intervalli 45/15), `0 */5` (ultimo intervallo 4h) e `*/0` vengono RIFIUTATI."""
campi = expr.split()
assert len(campi) == 5, expr
m, h, dom, mon, dow = campi
assert dom == mon == dow == "*", f"cadenza non regolare: {expr}"
if h == "*" and m.isdigit():
return 60
if h == "*" and m.startswith("*/"):
passo = int(m[2:])
if passo <= 0 or 60 % passo:
raise ValueError(f"passo di minuti non regolare: {expr}")
return passo
if h.startswith("*/") and m.isdigit():
passo = int(h[2:])
if passo <= 0 or 24 % passo:
raise ValueError(f"passo di ore non regolare: {expr}")
return 60 * passo
raise ValueError(f"espressione non riconosciuta: {expr}")
def _docstring() -> str:
return ast.get_docstring(ast.parse(BOOK_EXECUTE.read_text())) or ""
def _cron_dichiarato() -> str:
"""La prima espressione crontab a 5 campi che `cron_book.sh` dichiara fra backtick — di
qualunque forma: e' `cadenza_minuti` a giudicarla, non questa regex (P4: un'espressione
presente ma non oraria deve fallire come 'non oraria', non come 'assente')."""
m = _RX_CRON5.search(CRON_BOOK.read_text())
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab fra backtick"
return m.group(1)
def _crontab() -> str | None:
"""Testo della crontab dell'utente, None se non leggibile (P5: distinto da 'vuota')."""
if not shutil.which("crontab"):
return None
try:
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
except Exception:
return None
return out.stdout if out.returncode == 0 else None
def _righe_attive(testo: str, marcatore: str) -> list[str]:
"""Le espressioni (5 campi) di TUTTE le righe attive che citano `marcatore`: due righe sono
due esecuzioni, e la seconda non deve nascondersi dietro la prima."""
return [" ".join(r.split()[:5]) for r in testo.splitlines()
if marcatore in r and not r.lstrip().startswith("#")]
# ---------------------------------------------------------------- regressione sul docstring
# La guardia non cerca due letterali («ogni ~230»): estrae OGNI prescrizione di cadenza scritta
# come «ogni/every N unita'» e pretende che valga 60 minuti. ⚠️ Limite dichiarato (P13): una
# guardia sui numeri non copre il ragionamento — una prosa che prescrive una cadenza senza la
# parola «ogni» passa. Il controllo positivo qui sotto dice cosa la guardia VEDE.
_NUMERI = {"un": 1, "una": 1, "due": 2, "tre": 3, "quattro": 4, "sei": 6, "otto": 8, "dodici": 12}
_RX_CADENZA = re.compile(
r"\b(?:ogni|every)\s*~?\s*(\d+|un'?|una|due|tre|quattro|sei|otto|dodici)?\s*'?\s*"
r"(minuti|minuto|minutes?|min|m|ore|ora|hours?|h)\b", re.I)
def prescrizioni_minuti(testo: str) -> list[int]:
"""Ogni «ogni N unita'» del testo, in minuti. «ogni ora» -> 60, «ogni ~230 minuti» -> 230."""
out = []
for m in _RX_CADENZA.finditer(testo):
n_raw, unita = m.group(1), m.group(2).lower()
n = 1 if n_raw is None else (int(n_raw) if n_raw.isdigit() else _NUMERI[n_raw.rstrip("'")])
out.append(n * (60 if unita.startswith(("or", "h")) else 1))
return out
def test_la_guardia_vede_le_prescrizioni_che_deve_vedere():
# controllo positivo (M15): i modi in cui il docstring potrebbe ri-prescrivere la riga 4h
assert prescrizioni_minuti("va lanciato ogni ~230 minuti con la feed") == [230]
assert prescrizioni_minuti("il giro va lanciato ogni quattro ore") == [240]
assert prescrizioni_minuti("every 4 hours, then every 230 min") == [240, 230]
assert prescrizioni_minuti("controllato ogni ora; ogni 60 minuti") == [60, 60]
assert prescrizioni_minuti("fino a 230m di latenza") == [] # non e' una prescrizione
def test_il_docstring_prescrive_solo_la_cadenza_oraria():
d = _docstring()
p = prescrizioni_minuti(d)
assert p, "il docstring deve contenere almeno una prescrizione di cadenza"
assert all(x == 60 for x in p), f"prescrizioni non orarie nel docstring: {p}"
assert re.search(r"CADENZA:\s*ORARIA", d), "il docstring deve dichiarare la cadenza ORARIA"
def test_il_docstring_dichiara_la_stessa_riga_del_cron():
# la riga di crontab citata nel docstring e' quella di cron_book.sh, non un'altra
d = _docstring()
assert _cron_dichiarato() in d
# ---------------------------------------------------------------- cron_book.sh
def test_cron_book_e_orario_sotto_la_griglia_skh_e_sotto_la_riga_4h():
c = cadenza_minuti(_cron_dichiarato())
assert c == 60
assert c < LTF_MIN, "la cadenza deve stare SOTTO la griglia di SKH01 (latenza <= 1 barra)"
assert c < RIGA_4H_MIN, "la riga 4h e' quella in cui il rotolante di BTC fa 2 scatti"
assert "ORARIA" in CRON_BOOK.read_text()
def test_cron_book_evita_il_minuto_tondo():
# vincolo 1 del commento di cron_book.sh: fuori dai ~26s del :00 (rate-limit per-IP)
minuto = int(_cron_dichiarato().split()[0])
assert minuto != 0
def test_cron_book_evita_lo_slot_di_release_deribit():
# vincolo 2 del commento di cron_book.sh — il motivo vero del :07 -> :47 (4 giri morti in
# 63 giorni): il giro del martedi' alle 09:MM deve cadere FUORI dallo slot dichiarato in
# venue_probe (derivato, non riscritto). Il :07 lo violava; se qualcuno tornasse a :15
# con docstring e crontab coerenti, gli altri test passerebbero e questo no.
minuto = int(_cron_dichiarato().split()[0])
base = datetime(2026, 9, 1, tzinfo=timezone.utc) # un lunedi'
martedi = base + timedelta(days=(VP.RELEASE_WEEKDAY - base.weekday()) % 7)
giro = martedi.replace(hour=VP.RELEASE_START_MIN // 60, minute=minuto)
assert not VP.in_release_window(giro), f"il giro :{minuto:02d} cade nello slot di release"
assert VP.in_release_window(giro.replace(minute=7)), "controllo positivo: il :07 ci cadeva"
@pytest.mark.parametrize("expr,atteso", [("47 * * * *", 60), ("*/30 * * * *", 30),
("7 */4 * * *", 240), ("0 */24 * * *", 1440)])
def test_il_parser_della_cadenza(expr, atteso):
assert cadenza_minuti(expr) == atteso
@pytest.mark.parametrize("expr", ["47 9 * * 2", "*/45 * * * *", "0 */5 * * *", "*/0 * * * *",
"47 * 1 * *"])
def test_il_parser_rifiuta_cio_che_non_e_regolare(expr):
with pytest.raises((ValueError, AssertionError)):
cadenza_minuti(expr)
# ---------------------------------------------------------------- la crontab che GIRA
def test_la_crontab_installata_e_quella_dichiarata_e_una_sola():
"""P16: il cron gira dalla macchina, non dal repo. TRE stati, non due (P5):
- crontab NON leggibile -> SALTATO (non verificabile qui);
- leggibile, e questo progetto non ci compare affatto -> SALTATO (macchina di sviluppo);
- leggibile con altre righe del progetto ma SENZA cron_book.sh attivo -> ROSSO: il libro
non e' schedulato dove il progetto e' installato (una riga commentata per manutenzione e
dimenticata e' esattamente questo caso).
E la riga deve essere UNA: due righe attive sono due esecuzioni l'ora."""
testo = _crontab()
if testo is None:
pytest.skip("crontab non leggibile: non verificabile qui")
if str(ROOT) not in testo and ROOT.name not in testo:
pytest.skip("la crontab non cita questo progetto: non e' la macchina che lo esegue")
righe = _righe_attive(testo, "cron_book.sh")
assert righe, "questa macchina esegue il progetto ma cron_book.sh NON e' schedulato"
assert len(righe) == 1, f"cron_book.sh schedulato {len(righe)} volte: {righe}"
assert righe[0] == _cron_dichiarato()
assert cadenza_minuti(righe[0]) == 60
def test_due_righe_attive_non_si_nascondono_dietro_la_prima():
finta = ("47 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
"# 7 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n"
"17 * * * * /x/PythagorasGoal/scripts/cron_book.sh\n")
assert _righe_attive(finta, "cron_book.sh") == ["47 * * * *", "17 * * * *"]
assert _righe_attive("# 47 * * * * /x/cron_book.sh\n", "cron_book.sh") == []

Some files were not shown because too many files have changed in this diff Show More