merge: ondata di ricerca 0822 + libro di bordo del book live

114 commit. Due cose diverse dentro lo stesso ramo.

RICERCA (scripts/research/, docs/) — sei ondate, §21-69, 0 candidati promossi.
Il valore e' difensivo e reale: quattro monitor forward rotti che avrebbero
fatto decidere un gate al contrario, un costo gia' in essere mai contato (il
funding dei perpetual, -2,16%/anno di drift), un punto singolo di guasto nel
trasporto degli allarmi, quattro soglie pubblicate falsificate. Il finding di
prim'ordine e' §58: l'obiettivo del progetto ha DUE definizioni operative in
uso che danno 33,9% contro 0,33% sulla stessa domanda.

PRODUZIONE (src/live/, scripts/live/, cron) — il libro di bordo:
- data/live/trades.db, i trade allineati col tempo. Erano salvati ma datati
  alla BARRA DI SEGNALE: 19 righe su 19 a 00:00:00, e un trade registrato sei
  giorni prima di essere eseguito. L'ora vera viveva solo in cron_book.log,
  gitignored e fuori dal backup.
- book_execute.py: ts_utc = ora vera del fill, bar_ts = barra, con test di
  regressione sulla sorgente.
- docs/journal/: una voce al giorno, quattro livelli separati per
  provenienza — numeri misurati, Lettura a regole tracciabili, Analisi di un
  modello, Nota dell'operatore.
- sync orario in cron_book, giornale e analista in cron_daily.

Strategia, pesi e config del libro INVARIATI su tutto il ramo. Nessun ordine.
695 test passano.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-23 17:48:24 +00:00
150 changed files with 68816 additions and 3 deletions
+4
View File
@@ -86,3 +86,7 @@ data/live/
# dati esterni di ricerca (on-chain CoinMetrics community, F&G) — non certificati, non in git
data/external/
data/options_daily/
# libro di bordo: il DB e' dato operativo (coperto dal backup), non codice
data/live/trades.db
data/cache_regime_capitale.json
+1120 -2
View File
File diff suppressed because it is too large Load Diff
+152
View File
@@ -0,0 +1,152 @@
# Ondata multi-agente 2026-08-22 — 20 filoni, branch `research/wave-0822`
**Mandato dell'operatore:** *"metti decine di agenti a fare analisi di nuove strategie possibili.
Lavora su branch separato. Lo scopo e' sempre lo stesso arrivare ai 50 giornalieri velocemente."*
**Esito in una riga: 0 candidati promossi, 1 difetto di produzione che avrebbe falsato un gate fra
36 giorni, 3 soglie pubblicate falsificate, 1 leva strutturale che nessun gate del progetto sa
vedere. Book, pesi, cron, config INVARIATI.**
Ledger completo per filone: `docs/research/RESULTS-0822.md`. Briefing dato agli agenti:
`docs/research/BRIEF-0822.md`. 21 script in `scripts/research/r0822_*.py` (15.295 righe, compilano
tutti, ognuno gira da solo).
## 0. Come e' stata condotta (e perche' conta)
Vincolo di macchina: **2 core**, 7 GB, e sulla stessa VPS gira il libro con **soldi veri**
(`cron_book` al minuto :07). Il briefing imponeva `nice -n 19`, budget 15 min/run, sola lettura su
`config/`, `data/live/`, `src/live/`, `scripts/live/`, cron. **Ha tenuto**: al picco di carico
(load 14 su 2 core) il giro delle 17:07 del libro e' girato in **18 secondi**, feed fresco, nessuna
azione, nessun OOM.
⚠️ **Fatto operativo da ricordare: il cron gira dal working tree.** Con questo branch attivo,
stanotte alle 00:30 il cron esegue il codice di QUESTO branch. Verificato che il diff vs `main`
tocca **solo** `scripts/research/` e `docs/research/` -> produzione identica. **E' anche il motivo
per cui nessuna riparazione e' stata applicata qui: sarebbe andata live senza una decisione.**
Il briefing dava agli agenti, **in anticipo**, i dieci modi in cui questo progetto si e' gia'
ingannato (null del de-levering, TP01 travestito, selezione sull'hold-out, fortuna d'ancora, Sharpe
implausibile, win-rate come knob, ...) e la lista dei filoni morti. **Sette agenti hanno catturato
un errore proprio prima di pubblicare**, e tre hanno refutato una propria ipotesi in corsa.
## 1. Il risultato che va agito — 4 monitor forward su 6 sono rotti
`fetch_hyperliquid` (e `resample_tf`) scrivono la barra del **giorno in corso**; `advance()` dei
monitor la consuma e **porta `last_ts` su di essa** -> le ore restanti di ogni giorno non entrano in
nessun rendimento registrato.
| monitor | barre coincidenti col replay | **min/giorno registrati** | gate |
|---|---|---|---|
| `paper_statarb` | **0/46** | **4** | **STATARB 27/09** |
| `paper_dvolspread` | 0/28 | **2** | DVOLSPREAD 24/10 |
| `paper_xsr` | 1/28 | **41** | XSR01 23/10 |
| `paper_portfolio` | 7/56 | 19 | — |
| `paper_prevday` | 1259/1319 | 1.438 | — |
| `paper_combo` | 7/37 | 1.402 | — (sano **per caso**: aspetta una borsa) |
**Il gate STATARB del 27/09 si ribalta su 2 criteri su 3**: Sharpe registrato **+1,96** contro
**−2,02** ricostruito, maxDD **2,2% contro 15,3%** (guardia <10%).
✅ **Ma la finestra non va persa**, ed e' misurato due volte in modo indipendente: il feed **non
riscrive le barre chiuse** (`paper_prevday` 1427/1488 barre identiche al bit dopo 62 notti; e le 6
barre di `paper_statarb` recuperate dopo il guasto EPERM del 09-15/07 coincidono al bit).
**-> La riparazione e' "riparare `advance()` + RIGENERARE", non "riparare e azzerare": nessuna data
di gate si sposta.**
**Guardia raccomandata** (non implementata): `ts_ultima_barra + cadenza <= mtime del file`, grazia
5 min. O(1), segnala 5/6, **tace sul sano**, controllo positivo sintetico 6/6 nei due versi.
⚠️ `monitor_health` non poteva vederlo: **una serie fresca, completa e sbagliata passa ogni controllo
di freschezza**. E `implausible_sharpe` esiste dal 26/07 ma **non e' mai stato puntato sulle serie
forward** (avrebbe segnalato Sharpe −15,8 su 28 barre).
## 2. Tre soglie pubblicate, falsificate con misure
1. **"XS01 serve ~$20k"** -> **non esiste soglia da min-order**. L'origine del numero era *"rumore di
arrotondamento"*, una stima a occhio del 19/06. Haircut ~0 gia' a **$200-600 allocati**.
**Meccanismo**: gli ordini sono **due popolazioni** — il ribilanciamento del segnale e' il 13%
degli ordini ma il **75% del nozionale** (ticket $13,65, passa sempre); la deriva del vol-target
e' l'87% degli ordini ma il **25% del nozionale** (ticket $0,64, il pavimento la taglia **ed e'
gratis**). *Contare gli ordini da' 16% e sembra un disastro; contare il nozionale da' 75%.*
Due agenti indipendenti ci sono arrivati per strade diverse.
⚠️ **Il "$20k di XS01" e il "$20k della decisione di venue" erano due cose diverse conflate in
CLAUDE.md.** Il primo cade; **il secondo regge intatto** — e' una decisione dell'operatore
sull'asse della rovina, non sull'eseguibilita'.
2. **"Slippage = rischio #1 di XSR01"** -> **refutato con margine 21x** al livello di liquidita'
odierno (mezzo-spread 0,7-1,0 bps contro i ~16 che servirebbero a uccidere l'edge).
3. **"BTC opzioni fuori a $3.000 (min 0.1 = $6.210/lotto)"** -> misurato sulla famiglia **inverse**,
che il conto (in USDC) **non puo' marginare**. La famiglia USDC-lineare ha lotti **10x piu'
piccoli**. Il muro resta, ma per il **prezzo del lotto**, non per quello che si credeva.
## 3. La leva: l'unica variabile che nessun gate del progetto sa vedere
**Lo Sharpe e' invariante alla scala** (misurato: 1,31 a ogni cella del knob) -> `deflated_sharpe` e
`marginal_vs_tp01` non falliscono sulla leva, **non la vedono**. E' per questo che in due mesi la
scala non e' mai stata esaminata.
Il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log. Il gradino
eseguibile 1,00x -> 1,25-1,50x vale **14,7 anni -> 12,9-11,6** al capitale-rendita (€500/mese, netto
fisco). **Lo scettico ha confermato il risultato di testa** e ha tolto la sua condizione bloccante:
sotto la lente wick accoppiata il maxDD prende un **ricarico moltiplicativo costante del ~3,5%**, non
un'amplificazione (il maxDD e' multi-giorno, il wick e' di un giorno).
**Il gradino resta NON autorizzato**: cadono una riserva su tre. Restano il drift stimato su 7,4 anni
(k* e' lineare nel suo errore) e la **coda assente dal dataset** — un solo giorno a −10% all'anno
porta k* da ~10x a **2x**.
📌 **Ma la lezione del 25/07 e' vera e non si trasferisce:** su una regola a **UN giorno**
(daily-loss, stop di conto) close-only e' **esattamente cieca** — 0,00 breach/anno contro 0,40-1,21
veri, rapporto **INF**. **Sul canale prop/funded la lente accoppiata resta obbligatoria.**
## 4. Cosa e' morto, e come
**Filoni chiusi con autopsia** (l'elenco per esteso e' nel ledger): adaptive-horizon (il "vincitore
adattivo" e' un lookback **costante** travestito), dealer-gamma (la gamba tradeable e' **incoerente**:
BTC non separa, ETH separa **al rovescio**), ortho-screen **7/7**, oi-pin (il max-pain **non batte
mai** la media a 7 giorni dello spot), term-structure (la pendenza e' un **termometro contemporaneo**),
skew direzionale (**il prezzo muove lo skew**, t 3,1-9,4 su 8/8 — non il contrario), flow-squeeze,
basis-calendar (il basis dei datati **E'** il funding del perp), alt-options (alt), xs-lite,
vrp-quote-vere.
Tre chiusure di famiglia che valgono per le prossime ondate:
- **Il filone funding si chiude sul QUARTO lato** (affollamento): misurato su **53.430 ore / 3 anni**,
non predice ne' direzione ne' volatilita'. E i futures datati non sono un quinto lato: sono lo
stesso lato quotato diversamente (+7,33% implicito contro +6,48% realizzato, premio incassabile
+0,85%/anno con **IC95 che contiene lo zero**).
- **La mean-reversion non risorge** sotto due conditioner mai provati (volume, shock 3σ): la selezione
in-sample sceglie il ramo di **continuazione** in entrambi i casi.
- 📌 **Uno screen largo su BTC/ETH direzionale NON PUO' passare il proprio gate.** Su 168 trial il
massimo Sharpe atteso dal **puro rumore** e' **1,572**, sopra il soffitto direzionale misurato
(~1,3). Non e' sfortuna, e' aritmetica. **Le prossime ondate o dichiarano famiglie molto piu'
piccole in anticipo, o cambiano meccanismo.**
⚠️ **Tre volte in questa ondata l'eseguibilita' a $600 NON e' stata il vincolo** (haircut 0,00-0,01;
lotti $7-242; margine ~$148). Dopo due mesi in cui il muro era sempre il capitale, adesso muoiono
tutti sull'edge. **E' un'informazione sul dove cercare.**
## 5. Regole nuove, e una ritirata
**Nuove:**
- **L'open interest non misura la negoziabilita'** — su Deribit USDC sono quasi **anti-correlati
(rho di rango −0,77)**: SOL_USDC e' 1ª per OI e ultima per quote a due lati (**9%**); BTC_USDC ha
**5** strumenti con OI>=100 e il **93%** dei put quotati.
- **Il deflated-Sharpe va calcolato DI SCREEN, non solo di famiglia** (168 trial, non 24).
- **Un placebo si controlla per BILANCIAMENTO DEL SEGNO prima di usarlo**: un null che concorda col
segnale solo nel 14% dei casi non e' un null, e' la strategia invertita — e fabbrica celle
"significative".
- **Il null del de-levering e' DEGENERE quando la variante e' una ri-scalatura** (`sh(k*base) ===
sh(base)`): il test che serve e' **iso-peso sullo Sharpe**.
- **Un gate che stampa `None` va verificato prima di dichiararlo "non girato".**
- **Su una finestra interamente post-hold-out, `marginal_vs_tp01` non puo' dare ADDS per
costruzione** — un NEUTRAL li' non e' un giudizio.
**Ritirata (era stata pubblicata poche ore prima, in questa stessa ondata):**
❌ *"Su equity a gradino un overlay giornaliero e' non-causale"* — **falso su questo motore**:
`backtest_signals` contabilizza al giorno d'**ingresso** in **291/293** trade, e la "riparazione" e'
un **no-op bit-exact**. Il "+0,04 fantasma" era la differenza fra due varianti **entrambe causali**,
e quella scartata era la **migliore sull'hold-out**. *Il costo di una regola derivata da un difetto
inesistente e' gia' stato pagato.*
## 6. Cio' che resta aperto (decisioni dell'operatore, non mie)
1. **Riparare `advance()` + rigenerare** le serie forward, prima del 27/09.
2. **Cablare la guardia D2** (`ts_ultima_barra + cadenza <= mtime`) in `monitor_health`.
3. **Il collettore della catena** raccoglie la famiglia inverse; ETH_USDC costa **+587 chiamate/giro
(+90%)** ed e' una decisione sul rate limit per-IP, che il 29/07 e' gia' costata un guasto.
4. **Rimisurare `f` di VRP01 term-structure-consistent** (il 42% del difetto e' li', e la regola per
prenderlo era gia' scritta il 03/07).
5. **XS01 e' eseguibile a ~$1.300 di conto**, non a $20k. Resta fuori **per la decisione di venue**,
che e' un'altra cosa. Se il piano cambia, si riapre prima.
+188
View File
@@ -0,0 +1,188 @@
# 2026-08-22 (sera) — Seconda ondata: XS01 fuori dalla sua finestra, e il canale funded ridimensionato di 5x
**Branch:** `research/wave-0822` · **9 filoni** · **0 candidati promossi** · **libro, pesi, cron,
config INVARIATI.**
Registro per filone: `docs/research/RESULTS-0822.md` (sezioni 21-29). Briefing: `BRIEF-0822.md`.
---
## Perche' una seconda ondata
La prima (20 filoni) si era chiusa con un risultato di testa — **PROP-ALLOC**: su un conto *funded*
riallocare per la **barriera di drawdown** invece che per lo Sharpe porta J da 0,335 a 0,738, con
**P(≥50 €/g) da €600 in 36 mesi = 42%**. Il percorso piu' veloce mai misurato in questo progetto.
E si era chiusa con l'agente che dichiarava contro se stesso il proprio limite: *"tutto il vantaggio
poggia sul drift di XS01 misurato sulla sua finestra di scoperta"*, con un gate che era **un'attesa di
anni** (*"non aprire un funded prima che XS01 abbia una finestra fuori dal 2024-2026"*).
La seconda ondata e' stata mirata su quello, piu' sui falsificatori che gli agenti stessi avevano
**nominato senza poterli eseguire**.
---
## 1. XS01 regge fuori dalla sua finestra — ed e' piu' forte li' (§21)
La finestra esisteva gia': gli stessi 19 ticker su **Binance spot 2021-01 → 2023-12**, che contiene
**LUNA e FTX**. Meccanismo **congelato**, nessuna griglia cercata.
**Mediana di fase +1,12 fuori campione contro +0,37 nella finestra di scoperta; differenza appaiata
+0,668, positiva in 10/10 fasi**, p=0,013 contro permutazione a fee zero, sopravvive a 30 bps/lato.
L'obiezione ovvia — *"Binance non e' la verita'"* — e' stata **misurata invece che aggirata**: quella
regola riguarda l'**ancoraggio di prezzo**, non un ranking cross-sezionale, e lo scarto USDT e' un
**fattore comune** che sparisce nello z-score. Lo stesso sleeve sui due venue da' **corr 0,9991**.
**Null "universo dove non dovrebbe funzionare"** (11 settoriali SPDR, 1998+, meccanismo congelato):
**−0,45, 0/10 fasi positive** → **terza conferma indipendente che il cross-sectional e'
crypto-specifico**.
## 2. Il falsificatore che l'autore ha nominato contro se stesso — eseguito, e non falsifica (§28)
La validazione di venue di §21 girava **solo sul 2024+**, cioe' senza il depeg USDT che il fuori
campione contiene. Soglia dichiarata da lui: `corr ≥ 0,99`.
**Corr mediana 0,9992, minima 0,9921. dSharpe appaiato −0,000. Il titolo passa da +1,12 a +1,11.**
Guardando il canale del danno — i ribilanciamenti — su 734 solo **10 cambiano una gamba su cinque**, e
**zero dentro la finestra del depeg**.
**Il punto di metodo:** la divergenza va **scomposta**, perche' le due componenti sono domande diverse.
La **comune** arriva a −39 bps nel depeg ed e' quella che lo z-score annulla; la **idiosincratica** —
l'unica che cambia il ranking — sale solo da 2,0 a 3,6 bps mediani. *Riportare solo la prima avrebbe
dato la risposta rassicurante e sbagliata.*
E ha trovato che **il caso peggiore del campione non e' il depeg: e' il 2025-10-10** (cascata di
liquidazioni sulle /USDT, INJ a 1.981 bps mentre BTC/ETH stanno a 21). **Il meccanismo temuto e'
REALE**; e' semplicemente caduto nella finestra di *scoperta*, non nel fuori campione.
## 3. Ma il numero funded si ridimensiona di 5x — e il colpevole non e' la finestra (§29)
Decomposizione a **un grado di liberta' per volta**:
| passo | J | Δ |
|---|---|---|
| il pubblicato (HL, 19 gambe, finestra di scoperta) | 0,739 | — |
| → venue Binance, **date allineate** | 0,732 | **−0,006** |
| → **13 gambe** | 0,426 | **−0,307** |
| → pannello lungo | 0,459 | +0,034 |
| → **fuori campione 2021-2023** | 0,520 | **+0,061** |
**Il 42% del numero pubblicato sta nelle 6 gambe che fuori campione non esistono** (ARB OP SUI APT SEI
TIA) — **e quelle 6 entrarono nell'universo nel 2026-06 guardando la liquidita' HL del 2024+, cioe'
dentro la finestra di scoperta.** La finestra, da sola, **migliora**. Allargare l'universo man mano
che le gambe nascono non recupera nulla.
**Il numero operativo: P(≥50 €/g) da €600 in 36 mesi = 7,8% [7,2–9,1%], P(zero) 25,5%** — contro il
42% pubblicato stamattina. Il peso 0,50 non sopravvive; la **regione [25%, 38%] e' entro il 3%
dell'ottimo su tutte e tre le finestre**.
**`GATE PROP-01`** sostituisce l'attesa di anni: **(a)** leggere il **listino della firm** — ≥10 delle
13 gambe shortabili, verifica a **€0** e **bloccante prima di ogni spesa**, *stessa classe
dell'errore GTAA01/PRIIPs*; **(b)** banda d'ancora di SKH01 sotto la lente prop, **2026-10-31**, PASS
se > +0,05; **(c)** un'eval HYRO $100k costa **$579 su un conto di $635**.
## 4. L'allarme sul peso di TP01 e' falsificato NEL VERSO (§26)
L'agente aveva registrato nel docstring, **prima di misurare**, che si aspettava di confermarlo.
Invece: **TP01 ~0,375 e' l'argmax sia sulla finestra che contiene il 2022 sia su quella che non lo
contiene**, e nel sinistro il libro con **meno** TP01 fa **meglio** — TP01 va flat mentre **SKH01 si
gira short e guadagna**. Test dei segni 12/12 (p=0,0005), 8/8 sul solo pre-2024.
**Null del de-levering, 7a occorrenza, in veste nuova** ("protezione dal crash"): a leva comune TP01
protegge davvero, ma **a iso-sopravvivenza meno TP01 paga di piu'** (payout $4.490 → $2.119,
monotono).
Tre cose che valgono piu' dell'allarme rimosso:
- **"2024-2026 non contiene un crash" e' falsa nella forma**: ha buy&hold a −60% di DD. Manca la
**taglia** (~2,3x sulla coda). La domanda giusta e' *"e se ne arriva uno due volte piu' grande"*.
- **Alle leve funded il 2022 non era una minaccia** (min equity −1,4/−2,4% contro barriera −6%): cio'
che uccide un conto a barriera e' la **regola a un giorno su una giornata qualunque**.
- **Il rischio vero dell'ottimo e' XS01, non TP01** — e l'agente ha demolito la propria ipotesi
contraria dentro la sessione.
## 5. Tre filoni chiusi con margine
**BOCPD (§23) — l'orizzonte adattivo e' CHIUSO DEFINITIVAMENTE.** Il falsificatore e' stato eseguito
con i rilevatori che nominava (Adams & MacKay + optimal partitioning causale) e **questa volta ha
avuto potenza**: 68/96 celle davvero adattive, contro 0 del primo tentativo. Con potenza, **0/68** su
entrambe le condizioni. **La decomposizione e' il risultato: l'adattivita' spiega il −7% del vantaggio
su TP01** (+0,326 del miglior lookback costante contro +0,305 dell'adattivo). *Il 100% e' "un
orizzonte piu' corto", non "adattarlo".*
**MAKER (§22) — il segno dipende da `<` contro `<=`.** Il passivo **vince sul 92-98% degli ordini e
perde sulla media** (5 ordini su 1535 fanno il 42% del danno). Forma speculare del profit-take di
VRP01. Tetto **$2,96/anno a $635** nel caso impossibile.
**SURFACE-RV (§27) — chiuso, e il meccanismo vale piu' del verdetto.** Censimento su 1,24 M quote:
**0 violazioni di monotonia su 1,1 M**, 1 di convessita' su 1,03 M che vale **$0,00**. E `|residuo|`
cresce **monotonamente col largo del mercato** → **l'incoerenza al mid *e'* la larghezza del mercato
guardata attraverso il mid.** Questo uccide il controfattuale USDC che l'agente stesso aveva
costruito.
## 6. BIN-FREQ: la direzione e' reale, la taglia e' selezione (§25)
Argmax **+0,112 in 23/23 ancore**, ma **mediana di famiglia +0,007** con 39/72 celle positive =
monetina. **L'argmax vale 16x la mediana: punta, non plateau.** Direzione solidissima (0/72 sul segno
inverso, trasferisce a SKH01_V1).
**Due premesse del mio briefing, misurate e refutate:** *"dimezza gli ordini"* → i nettati calano del
**17%** e i sotto-min-order **sono di TP01**; *"converti il risparmio di fee"* → **a fee zero
l'effetto resta l'87-110%**, quindi non e' risparmio di costo ma informazione sul rendimento lordo.
E la domanda che avevo posto era **il confronto sbagliato** (la banda del livello si cancella in un
disegno appaiato) — **ma la risposta corretta e' peggiore, non migliore, per il candidato**.
Riapertura pre-registrata: **non una data, una soglia di CAPITALE** ($5.600 / $53.000), perche' cio'
che fallisce e' il rapporto fra un costo **fisso** e un beneficio **proporzionale**.
## 7. Il critico: i difetti stanno nella CUCITURA, non nei filoni (§24)
**Una regola mia, pubblicata la sera stessa e ritirata poche ore dopo:** avevo scritto che
`dealer_net_gamma` e' *"il GEX col segno invertito"*. **Falso**, e la fonte gira su questa stessa VPS
(`cerbero-mcp/.../common/options.py:11-14` **dichiara la convenzione**). Verificato in due comandi.
La correlazione a −0,95 era esattamente cio' che una convenzione diversa **deve** produrre.
**E il difetto piu' grande dell'ondata: il deflated-Sharpe misurato ai suoi due estremi degeneri lo
stesso giorno da agenti che non si citano** — e **tre gate pre-registrati** ci poggiano sopra.
---
## Il filo che tiene insieme la giornata: `deflated_sharpe` non ha una regione utile in mezzo
**Tre agenti indipendenti, tre strade diverse, stessa conclusione.**
`altlib.deflated_sharpe` calcola `sr0 = sd(Sharpe dei trial) × mult(N)`: dipende dalla **varianza**
della griglia, non solo da N.
- **VOL-SIZE** (ondata 1) lo dichiara **VACUO**: 1,000 anche per il baseline.
- **BOCPD** lo misura dall'altro capo: **0,998 PASS** dentro la famiglia omogenea → **0,555 FAIL** con
l'sr0 di screen. *"Gonfiare N col padding alza N e non la varianza."*
- **CRITICO** trova che l'1,572 di ORTHO-SCREEN implica sd≈0,58, e che **le stesse 168 celle
dichiarate come 7 famiglie da 24 danno 1,15** — sotto il soffitto ~1,3: il verdetto *"impossibile
per aritmetica"* **si ribalta senza toccare un dato**, e la cura che suggerisce e' la manovra che il
progetto ha proibito il 30/07.
- **BIN-FREQ** chiude il cerchio dall'alto: su una famiglia **binaria** il DSR **riacquista potenza**
(0,977 contro baseline 0,834) perche' le celle scartano insiemi di trade molto diversi. *Non e' il
candidato a essere piu' solido: e' la famiglia a essere piu' larga.*
**Non basta contare i trial di screen: serve la VARIANZA di screen.** E il numero di trial dichiarati
resta una scelta dell'autore, cioe' l'unico posto dove barare e' indolore e invisibile — con in piu'
il fatto che ora sappiamo che **la stessa griglia da' verdetti opposti a seconda di come la si
partiziona**.
---
## Cosa NON e' stato fatto, e perche'
**Nessuna riparazione applicata.** Il cron esegue dalla **working tree**, che e' su questo branch: una
riparazione andrebbe live alle 00:30 senza una decisione dell'operatore. Il diff tocca **solo**
`scripts/research/`, `docs/` e `CLAUDE.md` (verificato).
Restano dell'operatore: **(1)** riparare `advance()` + rigenerare i monitor forward prima del gate
STATARB del **27/09**; **(2)** decidere del branch; **(3)** la verifica a costo zero che decide il
canale funded — **leggere il listino di una prop firm**.
📌 **E la risposta onesta al mandato, che al critico era stato chiesto di dare se vera: nessuna misura
di ricerca di questa giornata batte "versare".** L'effetto piu' grande misurato oggi vale
**€0,0113/giorno**; versare €500/mese vale **~1456x** quello. La ricerca ha smesso di essere il
vincolo binding il 26/07, e due ondate da 29 filoni lo hanno **confermato invece che ribaltarlo**.
@@ -0,0 +1,184 @@
# 2026-08-23 — Ondate 7-8 e il critico di chiusura: 17 filoni, 0 candidati, 3 numeri di testa riscritti
**Branch:** `research/wave-0822` · **Registro per filone:** `docs/research/RESULTS-0822.md` §41-57
**Libro, pesi, cron, config: INVARIATI.** `git diff main -- src/ config/ scripts/live/ scripts/cron_*.sh tests/` vuoto a ogni checkpoint.
## Perche' esiste questa voce
Il mandato era *"metti decine di agenti a fare analisi di nuove strategie possibili... arrivare ai 50
giornalieri velocemente"*. Le ondate 1-3 (§1-40) avevano gia' dato 0 candidati. Queste due ne
aggiungono 17 e chiudono con un agente incaricato di **attaccare l'ondata invece di estenderla**.
La risposta al mandato e' negativa, ed e' quantificata: **la somma di tutti i lead positivi vale
+0,036 €/giorno**, contro i **+71 punti di probabilita'** che valgono €250 → €500 al mese.
Questa voce non ripete il registro. Tiene le tre cose che il registro, per costruzione, non puo'
dire: cosa e' cambiato **nella memoria operativa**, cosa ho sbagliato **io**, e cosa resta all'operatore.
## 1. Il risultato che nessun filone cercava
§50 chiedeva *"qual e' il miglior terzo sleeve eseguibile a $635"* e ha trovato che **il candidato
piu' forte mai misurato dal progetto era gia' in produzione — in monitor da giugno, senza gate
pre-registrato e senza deflated-Sharpe.** Sette candidati, **sette muri diversi, e nessuno dei sette
e' una soglia di capitale**: non e' il conto piccolo a fermarli.
§54 gli ha scritto il gate mancante. Passa **8 condizioni su 10**, e le due che mancano sono la
stessa: **la cella che gira non e' quella che la selezione onesta sceglie** (quella viva fallisce il
DSR a 0,905), e la selezione onesta compra il libro **long-flat**, cioe' butta via la gamba per cui
PREVDAY fu promosso. *Non e' un parametro da ritoccare: sono due strategie diverse.*
Verifica mia sopra il filone: la serie e' **intatta, 1512 barre su 1512** — e' l'unico monitor che
il difetto `advance()` non tocca. Ma la finestra e' **0,172 anni** e li' l'**MDE e' 4,72 di Sharpe**:
il +2,11 forward **non e' distinguibile da zero** su 26 ingressi. **Qualunque cosa il gate decida,
non puo' appoggiarsi al numero forward.**
## 2. Il critico, e le tre cose che vanno riscritte
Prima di ogni accusa ha **riprodotto** con macchineria propria: i quattro muri **al dollaro**, il
funding (−2,1597%/anno), il dShFULL di §42, e che il muro **e'** esattamente `prelievo/perpetua` su
4 lenti su 4. **Il nucleo quantitativo dell'ondata regge.** Poi ha falsificato **due sue attese
scritte prima di misurare** e tre miei numeri.
**(a) Il muro non e' $313k: e' la mediana di una banda esplosiva.** SE del drift **5,151%/anno**;
+1 SE → $204.517, −1 SE → $709.753, **−2 SE → il traguardo non esiste a nessun capitale**.
«€500/mese → P(20a) 85%» diventa **~0% a −1 SE**. Meccanismo: il muro e' un `1/x` su una quantita'
(la rendita perpetua) che **si annulla prima del drift** → errore asimmetrico verso l'alto.
Il pezzo che brucia: **per otto ondate ho stampato accanto a quel numero la risoluzione MONTE CARLO
(0,7%)** — la precisione del simulatore — **e mai quella del suo ingresso, cento volte piu' grande.**
**(b) «Positivo in 24 ancore su 24» vale ~2 osservazioni.** Corr media fra le 24 ancore di TP01
**0,631 → N_eff 1,55**; fra le 10 fasi di XS01 **0,790 → N_eff 1,23**. E **non si salva sulla
differenza appaiata**: la componente comune non si cancella (N_eff 1,64-2,24) — era l'attesa a
priori del critico, **refutata**. Sulla stessa grandezza la **banda d'ancora e' 4,3x piu' stretta
dell'IC95, che contiene lo zero** (su XS01: 3,8x; due misure indipendenti, stesso fattore ~4).
⚠️ **Non cadono i verdetti** — poggiano anche su iso-vol, maxDD, selection-on-holdout, edge lordo.
**Cade la precisione dei numeri, non la direzione delle decisioni.**
**(c) Il gradino di leva a 1,50x era gia' morto.** §53 lo boccia (peggior giorno strutturale
**21,48% > 20%**) e fissa **k max difendibile 1,40**. Sopravvive solo il **1,25x**, che vale
**+€144/mese misurati direttamente** (non €164, che veniva da un'interpolazione lineare) e
**+1,8 anni a muro congelato / +3,0 a muro mobile** — *e la convenzione va sempre detta, perche' le
due letture distano piu' di quasi tutti gli effetti che questo progetto misura.*
**In piu': tre baseline sotto lo stesso nome «libro 75/25».** `hourly` 1,683/11,22% (la lente di
**ogni muro e traiettoria**) · `canonical` 1,800/9,56% · mediana d'ancora 1,626/10,4%. **Spread 0,12
di Sharpe, piu' grande di quasi tutti i Δ dell'ondata**: un Δ accostato al baseline sbagliato cambia
piu' del Δ. **Ogni numero va scritto con la sua lente attaccata.**
## 3. Cosa ho sbagliato io
Cinque errori, tutti su numeri **gia' pubblicati**, due dei quali stavano in `CLAUDE.md`:
| # | cosa avevo scritto | cos'e' |
|---|---|---|
| a | «la convinzione di TP01 sta spesso a 1/3 o 2/3» | **il bucket 2/3 non esiste** — `tsmom_blend` media tre `np.sign()`, la famiglia ha **un** grado di liberta', non tre. Trovato da **due agenti indipendenti** |
| b | «la catena USDC costa +90% di chiamate» | **+18%** — i 587 erano il conteggio grezzo, senza il filtro OI≥100 che il collettore applica |
| c | «le opzioni BTC sono fuori a $3.000 per il lotto» | quello e' il **collaterale per VENDERE**; comprare costa il **premio**. Il muro vero e' il **tick da 5,00 USDC** |
| d | «il gradino vale +1,8a / +€164» | lettura a **muro fermo**, convenzione **mai dichiarata** |
| e | «la coda muove k\* di 1-2 unita'» | **4,6** |
E uno catturato **prima** di pubblicarlo, che vale la pena tenere perche' e' della famiglia che
stavo criticando: avevo formulato l'MDE di PREVDAY come *«la precisione apparente viene dal numero
di mark, non di scommesse»*. **Falso**: la SE dello Sharpe annualizzato va con `1/sqrt(anni)`
**indipendentemente dalla frequenza**, e infatti barra e trade danno lo stesso 4,72. Il fatto vero
e' piu' banale: **63 giorni sono 63 giorni.**
Il critico ha fatto lo stesso su di se': la sua prima stesura **de-luckava una serie gia'
de-luckata** (0,89²) e stampava un finto +31,5% sul muro. *Il controllo che l'ha preso e' stato
guardare il DRIFT stampato accanto al muro, non il muro.*
## 4. Due difetti di contabilita' nel piano, entrambi a favore
- **«€X/mese» versa ogni 30 GIORNI** → 121/182/243 versamenti invece di 120/180/240
(+0,83/+1,11/+1,25%), in **ogni tabella dal 25/07**.
- **Il contatore `versato` e' uno scalare che non si ferma al traguardo** → il *«a 10 anni i bonifici
fanno il 73%»* e' l'intero orizzonte; **fino all'arrivo e' il 61%** ($190.265). *Stessa famiglia
del bug `paid` catturato il 25/07 — la lezione era codificata e il difetto era in un altro punto.*
## 5. Cosa e' stato refutato, e perche' e' interessante
Tre meccanismi di protezione, **due caduti sulla premessa prima che sul prezzo**:
- **TAIL-HEDGE (§46):** comprare put deep-OTM fa **SALIRE** il maxDD in **162/162 celle**, a ogni
lente di f — perche' il beta del libro al sottostante e' **+0,076**. *Non si assicura un libro che
nei crash e' gia' quasi piatto.*
- **TP01-TWIN (§49):** un ensemble di definizioni di trend protegge **peggio** (0/24 ancore, 3,2x
peggio nel 2022) — e i meccanismi **non erano ridondanti** (corr di posizione 0,61): la ridondanza
non era il problema.
- **ANCHOR-ENSEMBLE (§47):** su TP01 il drift **non puo'** cambiare, per **algebra**
(`lordo_ens == media(lordi)`, residuo 1,4e-17).
E il lead dello spot (§52) **muore dove servirebbe**: regge a $600, **si annulla a $272k** — sparisce
esattamente al muro che pretende di spostare.
## 6. MDE: quanti «SCARTATI» sono in realta' «non misurabile»
Il critico ha misurato la finestra della catena opzioni: **77 giorni di superficie** → **MDE 4,3 di
Sharpe**. Ogni SCARTATO di §4/§5/§7/§11 che poggia su un confronto di Sharpe e' un **non-risultato
su quell'asse** (le loro parti valide sono lead-lag, placebo e censimenti). L'ondata dichiara il
proprio MDE in §6, §10, §43, §48, §56 — **disciplina reale** — ma tre etichette restano piu' forti
della misura, e una conta: 🚨 **§21 XS01 fuori campione, il PILASTRO del canale funded, ha effetto
+1,12 contro un MDE di 1,13.** Sta *esattamente* al proprio limite di rilevabilita', e viene
propagato fino a P(≥50 €/g) come se fosse noto a due cifre. *(Equita': gira anche un null di
permutazione a fee zero, p=0,013, che ha piu' potenza del t-stat. Il problema non e' «nessuna
evidenza», e' la precisione attribuita al livello.)*
## 7. La risposta al mandato
| lead | €/giorno a $635 | eseguibile oggi |
|---|---|---|
| gradino di leva 1,25x | **+0,046** | no — la manopola non esiste |
| spot al posto del perpetual | +0,017 | no — domanda fiscale aperta |
| PREVDAY al 15% (de-luckato) | +0,014 | no — gira la cella sbagliata |
| binario di frequenza | +0,011 | no — 3 gate su 4 falliti |
| esecuzione passiva | +0,005 | no — l'IC contiene lo zero |
| **funding (costo scoperto)** | **−0,023** | **lo paghi gia'** |
**Somma di tutti i lead positivi, se fossero autorizzati e additivi — non lo sono, e nessuno e'
eseguibile oggi: +0,036 €/giorno.** Sullo stesso motore e sugli stessi percorsi, **€250 → €500 al
mese porta P(20a) dal 14% all'85%** e la mediana da 23,4 a 17,3 anni. **Rapporto ~1.400 a 1.**
🚨 **`L'ONDATA NON HA PRODOTTO NIENTE CHE AVVICINI I 50 €/GIORNO.`** Ha prodotto (i) un **costo gia'
in essere** che porta P(20a) a €250/mese dal 49% al 14% (il funding), (ii) **quattro monitor rotti**
che avrebbero fatto decidere un gate al contrario, (iii) un **punto singolo di guasto** nel trasporto
degli allarmi, (iv) **tre lead reali tutti bloccati da qualcosa che non e' la ricerca**.
**Il suo valore e' difensivo ed e' reale: ha impedito decisioni sbagliate.** E nulla di sepolto sotto
uno «SCARTATO» merita di essere riesumato.
## 8. A credito dell'ondata
**Quattro auto-correzioni in 24 ore**, ed e' il suo dato piu' sano: §45 «lo spot non si liquida»
falsificato da §52; §39 «non esiste una linea USDC datata» falsificato lo stesso giorno; la regola
«overlay non causale» **ritirata** dallo scettico; «`dealer_net_gamma` e' il GEX invertito» ritirata
**leggendo il sorgente** che gira su questa stessa VPS.
## 9. Cosa resta all'operatore
Sette decisioni, nessuna delle quali richiede altra ricerca. Brief:
`https://claude.ai/code/artifact/9ca378f7-464a-4ee1-a520-38a026a34fc3`
1. **Riparare `advance()` e rigenerare** prima del gate STATARB del **27/09** (35 giorni) — oggi il
gate si deciderebbe **al contrario** (Sharpe +1,96 registrato contro −2,02 vero).
2. **Che fare del branch** `research/wave-0822` (106 commit, tutti in `scripts/research/` e `docs/`).
3. **Chiedere alla firm** se ha una lista ristretta propria (costo €0, blocca una spesa di $579).
4. **Chiedere al commercialista** se i derivati Deribit sono al 26% o al 33% (vale ~$22k di muro).
5. **Costruire la chiave di scala** in config, se si vuole il gradino **1,25x** (e **solo** quello).
6. **Riparare il trasporto degli allarmi** — la parte (c) cambia il comportamento, quindi e' una scelta.
7. **Decidere di `paper_prevday`**: allinearlo alla cella onesta, ritirarlo, o lasciarlo scrivendo
che la sua finestra non e' evidenza.
## Regole nuove
1. **Un muro calcolato come `X/y` con `y` stimato va pubblicato con la banda di `y`**, non con la
risoluzione del simulatore — e se `y` si annulla prima del suo ingresso, l'errore e' **asimmetrico**
e la banda non e' simmetrica attorno al punto.
2. **«N/N ancore» e' robustezza alla SCELTA dell'ancora, ~2 osservazioni indipendenti — non N prove.**
La banda d'ancora **non e' un intervallo di confidenza** e non va usata al posto di uno; e **non si
attacca un p-value** a un conteggio di ancore o di finestre sovrapposte.
3. **Un Δ si scrive con la sua BASELINE attaccata.** Tre lenti dello stesso libro distano 0,12 di
Sharpe, piu' della maggior parte dei Δ misurati: la lente sbagliata cambia piu' del risultato.
4. **Un argomento refutato due volte per due strade indipendenti non va ereditato come regola**
(il «soffitto ~1,3» e l'aritmetica del DSR di screen che ne discendeva).
5. **Prima di chiudere una famiglia, confrontare l'etichetta con il proprio MDE dichiarato** — §48
chiude un filone su una branca che dichiara essa stessa un fattore 44 sotto l'MDE.
6. **Un contatore cumulativo dentro un simulatore va fermato all'evento che sta misurando**, o
risponde a una domanda diversa da quella posta (2ª occorrenza in un mese, in due punti diversi).
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-23
*Scritto 2026-08-23T13:34:09+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,684.00 | -1.99% | -4.48% | -18.57% | 42.8% | ↓ ↓ ↓ (0/3 su) | 41.6 (27° pctl 1a) |
| **ETH** | $1,665.60 | -3.52% | -7.05% | -20.59% | 63.4% | ↓ ↓ ↓ (0/3 su) | 56.2 (21° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-23 · 2 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (2 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 2/24 — **22 mancanti**
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 41.6 contro realizzata 30g 42.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).
- `[vol]` ETH: implicita 56.2 contro realizzata 30g 63.4 (sotto); DVOL al 21° pctl di un anno; IV-rank espandente 0.15 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[giri]` ⚠️ **22 giri di `book_execute` mancanti** su 24: in quelle ore il libro non ha ne' letto ne' eseguito.
## Nota
*(vuota — campo dell'operatore)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-24
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $60,977.50 | -2.72% | -5.39% | -21.09% | 43.3% | ↓ ↓ ↓ (0/3 su) | 45.1 (51° pctl 1a) |
| **ETH** | $1,619.90 | -2.74% | -7.43% | -23.29% | 63.6% | ↓ ↓ ↓ (0/3 su) | 58.8 (28° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-24 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 45.1 contro realizzata 30g 43.3 (sopra); DVOL al 51° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 58.8 contro realizzata 30g 63.6 (sotto); DVOL al 28° pctl di un anno; IV-rank espandente 0.19 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **1 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-25
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $59,702.00 | -2.09% | -5.08% | -21.25% | 43.3% | ↓ ↓ ↓ (0/3 su) | 46.6 (57° pctl 1a) |
| **ETH** | $1,564.90 | -3.40% | -8.47% | -24.43% | 64.1% | ↓ ↓ ↓ (0/3 su) | 60.8 (33° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-25 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 46.6 contro realizzata 30g 43.3 (sopra); DVOL al 57° pctl di un anno; IV-rank espandente 0.24 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 60.8 contro realizzata 30g 64.1 (sotto); DVOL al 33° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **2 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-26
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $60,017.00 | +0.53% | -5.47% | -19.28% | 43.4% | ↓ ↓ ↓ (0/3 su) | 45.5 (52° pctl 1a) |
| **ETH** | $1,576.05 | +0.71% | -7.82% | -22.08% | 64.1% | ↓ ↓ ↓ (0/3 su) | 59.8 (30° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-26 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 45.5 contro realizzata 30g 43.4 (sopra); DVOL al 52° pctl di un anno; IV-rank espandente 0.23 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 59.8 contro realizzata 30g 64.1 (sotto); DVOL al 30° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **3 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-27
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $59,956.00 | -0.10% | -6.67% | -18.47% | 43.4% | ↓ ↓ ↓ (0/3 su) | 45.2 (52° pctl 1a) |
| **ETH** | $1,570.80 | -0.33% | -9.67% | -21.76% | 64.2% | ↓ ↓ ↓ (0/3 su) | 59.9 (31° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-27 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 45.2 contro realizzata 30g 43.4 (sopra); DVOL al 52° pctl di un anno; IV-rank espandente 0.22 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 59.9 contro realizzata 30g 64.2 (sotto); DVOL al 31° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **4 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-28
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $59,503.50 | -0.75% | -5.92% | -18.92% | 43.4% | ↓ ↓ ↓ (0/3 su) | 47.7 (63° pctl 1a) |
| **ETH** | $1,569.20 | -0.10% | -8.01% | -22.04% | 64.1% | ↓ ↓ ↓ (0/3 su) | 61.3 (37° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-28 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 47.7 contro realizzata 30g 43.4 (sopra); DVOL al 63° pctl di un anno; IV-rank espandente 0.27 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 61.3 contro realizzata 30g 64.1 (sotto); DVOL al 37° pctl di un anno; IV-rank espandente 0.23 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **5 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-29
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $60,166.50 | +1.11% | -5.92% | -18.46% | 43.6% | ↓ ↓ ↓ (0/3 su) | 42.6 (33° pctl 1a) |
| **ETH** | $1,611.20 | +2.68% | -6.68% | -20.24% | 65.1% | ↓ ↓ ↓ (0/3 su) | 57.9 (26° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-29 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (23 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 42.6 contro realizzata 30g 43.6 (sotto); DVOL al 33° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 57.9 contro realizzata 30g 65.1 (sotto); DVOL al 26° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **6 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-06-30
*Scritto 2026-08-23T13:34:10+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $58,546.50 | -2.69% | -6.60% | -20.44% | 44.2% | ↓ ↓ ↓ (0/3 su) | 44.5 (46° pctl 1a) |
| **ETH** | $1,570.55 | -2.52% | -5.71% | -21.67% | 65.4% | ↓ ↓ ↓ (0/3 su) | 59.0 (29° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-06-30 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 44.5 contro realizzata 30g 44.2 (sopra); DVOL al 46° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 59.0 contro realizzata 30g 65.4 (sotto); DVOL al 29° pctl di un anno; IV-rank espandente 0.19 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **7 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-01
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $59,961.50 | +2.42% | -1.67% | -15.93% | 44.6% | ↓ ↓ ↓ (0/3 su) | 42.9 (36° pctl 1a) |
| **ETH** | $1,607.95 | +2.38% | -0.74% | -19.81% | 66.3% | ↓ ↓ ↓ (0/3 su) | 56.9 (23° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-01 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 42.9 contro realizzata 30g 44.6 (sotto); DVOL al 36° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 56.9 contro realizzata 30g 66.3 (sotto); DVOL al 23° pctl di un anno; IV-rank espandente 0.16 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **8 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-02
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $61,518.50 | +2.60% | +3.04% | -7.71% | 40.0% | ↓ ↓ ↓ (0/3 su) | 40.5 (21° pctl 1a) |
| **ETH** | $1,698.60 | +5.64% | +8.54% | -8.57% | 64.9% | ↓ ↓ ↓ (0/3 su) | 55.1 (16° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-02 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 40.5 contro realizzata 30g 40.0 (sopra); DVOL al 21° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 55.1 contro realizzata 30g 64.9 (sotto); DVOL al 16° pctl di un anno; IV-rank espandente 0.14 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **9 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-03
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,542.00 | +1.66% | +4.21% | -2.28% | 38.1% | ↓ ↓ ↓ (0/3 su) | 39.5 (14° pctl 1a) |
| **ETH** | $1,757.15 | +3.45% | +11.49% | -2.98% | 65.6% | ↓ ↓ ↓ (0/3 su) | 53.5 (8° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-03 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 39.5 contro realizzata 30g 38.1 (sopra); DVOL al 14° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.5 contro realizzata 30g 65.6 (sotto); DVOL al 8° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **10 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-04
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,080.00 | +0.86% | +5.21% | -1.15% | 38.2% | ↓ ↓ ↓ (0/3 su) | 39.2 (13° pctl 1a) |
| **ETH** | $1,779.40 | +1.27% | +13.28% | +0.56% | 65.3% | ↑ ↓ ↓ (1/3 su) | 53.0 (6° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-04 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 39.2 contro realizzata 30g 38.2 (sopra); DVOL al 13° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.0 contro realizzata 30g 65.3 (sotto); DVOL al 6° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **11 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-05
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,624.00 | +0.86% | +6.92% | +4.22% | 34.9% | ↑ ↓ ↓ (1/3 su) | 38.7 (10° pctl 1a) |
| **ETH** | $1,784.95 | +0.31% | +13.75% | +12.77% | 51.3% | ↑ ↓ ↓ (1/3 su) | 53.1 (6° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-05 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 38.7 contro realizzata 30g 34.9 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.1 contro realizzata 30g 51.3 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **12 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-06
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,012.00 | +0.61% | +6.39% | +5.19% | 34.8% | ↑ ↓ ↓ (1/3 su) | 37.3 (4° pctl 1a) |
| **ETH** | $1,798.85 | +0.78% | +11.65% | +14.67% | 51.1% | ↑ ↓ ↓ (1/3 su) | 52.6 (6° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-06 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 37.3 contro realizzata 30g 34.8 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.6 contro realizzata 30g 51.1 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **13 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-07
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,322.00 | -1.08% | +8.16% | +0.03% | 32.3% | ↑ ↓ ↓ (1/3 su) | 38.9 (12° pctl 1a) |
| **ETH** | $1,770.30 | -1.59% | +12.72% | +4.79% | 45.0% | ↑ ↓ ↓ (1/3 su) | 53.3 (7° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-07 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (23 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 38.9 contro realizzata 30g 32.3 (sopra); DVOL al 12° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.3 contro realizzata 30g 45.0 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **14 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-08
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,259.00 | -1.68% | +3.83% | -1.27% | 32.8% | ↓ ↓ ↓ (0/3 su) | 39.5 (16° pctl 1a) |
| **ETH** | $1,742.25 | -1.58% | +8.35% | +3.14% | 45.4% | ↑ ↓ ↓ (1/3 su) | 53.4 (8° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 39.5 contro realizzata 30g 32.8 (sopra); DVOL al 16° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.4 contro realizzata 30g 45.4 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **15 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-09
*Scritto 2026-08-23T13:34:11+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,200.00 | +1.51% | +2.73% | +2.42% | 32.3% | ↑ ↓ ↓ (1/3 su) | 37.3 (5° pctl 1a) |
| **ETH** | $1,744.25 | +0.11% | +2.69% | +6.49% | 43.9% | ↑ ↓ ↓ (1/3 su) | 52.0 (5° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (15 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 37.3 contro realizzata 30g 32.3 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.0 contro realizzata 30g 43.9 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **16 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-10
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,136.00 | +1.48% | +2.55% | +4.36% | 32.6% | ↑ ↓ ↓ (1/3 su) | 36.2 (2° pctl 1a) |
| **ETH** | $1,796.05 | +2.97% | +2.21% | +10.82% | 44.7% | ↑ ↓ ↓ (1/3 su) | 49.8 (4° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 36.2 contro realizzata 30g 32.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.8 contro realizzata 30g 44.7 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **17 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-11
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,804.50 | -0.52% | +1.15% | +0.36% | 30.5% | ↑ ↓ ↓ (1/3 su) | 36.8 (4° pctl 1a) |
| **ETH** | $1,787.15 | -0.50% | +0.44% | +6.93% | 43.7% | ↑ ↓ ↓ (1/3 su) | 50.6 (4° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 36.8 contro realizzata 30g 30.5 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.6 contro realizzata 30g 43.7 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **18 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-12
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,759.50 | -0.07% | +0.21% | +0.33% | 30.5% | ↑ ↓ ↓ (1/3 su) | 37.2 (5° pctl 1a) |
| **ETH** | $1,805.70 | +1.04% | +1.16% | +8.40% | 43.7% | ↑ ↓ ↓ (1/3 su) | 51.0 (5° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 37.2 contro realizzata 30g 30.5 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.0 contro realizzata 30g 43.7 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **19 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-13
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,265.50 | -2.34% | -2.73% | -3.34% | 31.2% | ↓ ↓ ↓ (0/3 su) | 37.8 (8° pctl 1a) |
| **ETH** | $1,775.55 | -1.67% | -1.30% | +5.67% | 44.2% | ↑ ↓ ↓ (1/3 su) | 51.1 (6° pctl 1a) |
## Libro
Equity **$598.06** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+0.00**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 37.8 contro realizzata 30g 31.2 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.1 contro realizzata 30g 44.2 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **20 giorni, 0 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-14
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $65,012.00 | +4.41% | +2.67% | -1.08% | 34.1% | ↓ ↓ ↓ (0/3 su) | 36.5 (3° pctl 1a) |
| **ETH** | $1,889.60 | +6.42% | +6.74% | +9.53% | 48.3% | ↑ ↓ ↓ (1/3 su) | 49.5 (3° pctl 1a) |
## Libro
Equity **$598.49** · nozionale lordo $75 · leva lorda 0.13x · barra dati 2026-07-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+75 | HOLD (a target) |
## P&L
- giorno: **$+0.43** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 1 fill · fee 0.0374
- cumulato dall'arming: **$+0.43**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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 ETH $+75 (TP01 +0.000, SKH01 long).
- `[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 $+0.43 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto).
- `[vol]` BTC: implicita 36.5 contro realizzata 30g 34.1 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.5 contro realizzata 30g 48.3 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +4.41%, **2.5 deviazioni** giornaliere (sd implicita dalla RV30 = 1.78%).
- `[evento]` 📌 ETH: giornata a +6.42%, **2.5 deviazioni** giornaliere (sd implicita dalla RV30 = 2.53%).
- `[evidenza]` 📌 Campione a oggi: **21 giorni, 0 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)*
+45
View File
@@ -0,0 +1,45 @@
# Giornale di bordo — 2026-07-15
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,734.50 | -0.43% | +3.98% | -2.35% | 34.0% | ↓ ↓ ↓ (0/3 su) | 36.0 (1° pctl 1a) |
| **ETH** | $1,916.70 | +1.43% | +10.01% | +6.73% | 46.6% | ↑ ↓ ↓ (1/3 su) | 49.0 (2° pctl 1a) |
## Libro
Equity **$600.14** · nozionale lordo $77 · leva lorda 0.13x · barra dati 2026-07-15 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+77 | HOLD (a target) |
## P&L
- giorno: **$+1.65** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$+2.08**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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 ETH $+77 (TP01 +0.000, SKH01 long).
- `[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 $+1.65 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 36.0 contro realizzata 30g 34.0 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.0 contro realizzata 30g 46.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **22 giorni, 0 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)*
+45
View File
@@ -0,0 +1,45 @@
# Giornale di bordo — 2026-07-16
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,804.50 | -1.44% | +0.96% | -2.77% | 34.1% | ↓ ↓ ↓ (0/3 su) | 36.4 (3° pctl 1a) |
| **ETH** | $1,863.55 | -2.77% | +6.84% | +4.00% | 47.7% | ↑ ↓ ↓ (1/3 su) | 49.2 (2° pctl 1a) |
## Libro
Equity **$597.93** · nozionale lordo $75 · leva lorda 0.13x · barra dati 2026-07-16 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+75 | HOLD (a target) |
## P&L
- giorno: **$-2.21** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.13**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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 ETH $+75 (TP01 +0.000, SKH01 long).
- `[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 $-2.21 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[vol]` BTC: implicita 36.4 contro realizzata 30g 34.1 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.2 contro realizzata 30g 47.7 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **23 giorni, 0 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-17
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,904.00 | +0.16% | -0.36% | -0.85% | 33.6% | ↓ ↓ ↓ (0/3 su) | 36.3 (3° pctl 1a) |
| **ETH** | $1,841.00 | -1.21% | +2.50% | +5.21% | 47.1% | ↑ ↓ ↓ (1/3 su) | 49.0 (2° pctl 1a) |
## Libro
Equity **$596.77** · nozionale lordo $74 · leva lorda 0.12x · barra dati 2026-07-17 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | LONG @ 1,868.9 | $+75 | $+74 | HOLD (a target) |
## P&L
- giorno: **$-1.16** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.29**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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 ETH $+74 (TP01 +0.000, SKH01 long).
- `[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 $-1.16 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.63%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.3 contro realizzata 30g 33.6 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.0 contro realizzata 30g 47.1 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **24 giorni, 0 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-18
*Scritto 2026-08-23T13:34:12+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,810.00 | +1.42% | +1.58% | +3.04% | 32.8% | ↑ ↓ ↓ (1/3 su) | 35.7 (1° pctl 1a) |
| **ETH** | $1,861.75 | +1.13% | +4.17% | +8.89% | 46.4% | ↑ ↓ ↓ (1/3 su) | 49.3 (4° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-18 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.15** di equity (24 letture)
- realizzato: -1.10 netto su 1 round-trip chiusi · 1 fill · fee 0.0369
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.15 con 1 fill e 1 round-trip chiusi (realizzato $-1.10 netto).
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.7 contro realizzata 30g 32.8 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.3 contro realizzata 30g 46.4 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **25 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-19
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,704.50 | -0.16% | +1.48% | +1.92% | 32.7% | ↑ ↓ ↓ (1/3 su) | 36.1 (3° pctl 1a) |
| **ETH** | $1,871.60 | +0.53% | +3.65% | +9.47% | 46.4% | ↑ ↓ ↓ (1/3 su) | 49.7 (6° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-19 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.1 contro realizzata 30g 32.7 (sopra); DVOL al 3° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.7 contro realizzata 30g 46.4 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **26 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-20
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $65,216.50 | +0.79% | +4.74% | +1.52% | 32.5% | ↑ ↓ ↓ (1/3 su) | 36.3 (4° pctl 1a) |
| **ETH** | $1,903.45 | +1.70% | +7.20% | +9.46% | 46.4% | ↑ ↓ ↓ (1/3 su) | 50.4 (7° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-20 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.3 contro realizzata 30g 32.5 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.4 contro realizzata 30g 46.4 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **27 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-21
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $66,532.50 | +2.02% | +2.34% | +5.19% | 32.7% | ↑ ↓ ↓ (1/3 su) | 37.3 (10° pctl 1a) |
| **ETH** | $1,929.10 | +1.35% | +2.09% | +13.09% | 45.8% | ↑ ↓ ↓ (1/3 su) | 51.0 (8° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-21 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.3 contro realizzata 30g 32.7 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.0 contro realizzata 30g 45.8 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **28 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-22
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $66,081.00 | -0.68% | +2.08% | +3.32% | 32.6% | ↑ ↓ ↓ (1/3 su) | 38.0 (12° pctl 1a) |
| **ETH** | $1,933.70 | +0.24% | +0.89% | +12.00% | 45.7% | ↑ ↓ ↓ (1/3 su) | 51.6 (10° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-22 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 38.0 contro realizzata 30g 32.6 (sopra); DVOL al 12° pctl di un anno; IV-rank espandente 0.07 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.6 contro realizzata 30g 45.7 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **29 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-23
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $65,060.00 | -1.55% | +1.97% | +3.79% | 32.3% | ↑ ↓ ↓ (1/3 su) | 38.5 (14° pctl 1a) |
| **ETH** | $1,877.45 | -2.91% | +0.75% | +12.72% | 45.1% | ↑ ↓ ↓ (1/3 su) | 51.6 (10° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-23 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 38.5 contro realizzata 30g 32.3 (sopra); DVOL al 14° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.6 contro realizzata 30g 45.1 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **30 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-24
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,103.50 | -1.47% | +0.31% | +5.13% | 31.1% | ↑ ↓ ↓ (1/3 su) | 37.0 (7° pctl 1a) |
| **ETH** | $1,860.50 | -0.90% | +1.06% | +14.85% | 43.9% | ↑ ↓ ↓ (1/3 su) | 50.5 (7° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-24 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## Salute
- giri di `book_execute`: 24/24
- eta' feed SKH all'ultimo giro: non misurata
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.0 contro realizzata 30g 31.1 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.5 contro realizzata 30g 43.9 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **31 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-25
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,327.50 | +0.35% | -0.74% | +7.75% | 30.0% | ↑ ↓ ↓ (1/3 su) | 37.7 (11° pctl 1a) |
| **ETH** | $1,873.45 | +0.70% | +0.63% | +19.72% | 41.5% | ↑ ↓ ↓ (1/3 su) | 51.5 (10° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-25 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.7 contro realizzata 30g 30.0 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.5 contro realizzata 30g 41.5 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **32 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-26
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $65,357.50 | +1.60% | +1.01% | +8.90% | 30.4% | ↑ ↓ ↓ (1/3 su) | 37.3 (10° pctl 1a) |
| **ETH** | $1,953.65 | +4.28% | +4.38% | +23.96% | 43.4% | ↑ ↓ ↓ (1/3 su) | 51.7 (11° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-26 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.3 contro realizzata 30g 30.4 (sopra); DVOL al 10° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.7 contro realizzata 30g 43.4 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **33 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-27
*Scritto 2026-08-23T13:34:13+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,710.50 | -2.52% | -2.31% | +6.26% | 31.9% | ↑ ↓ ↓ (1/3 su) | 37.5 (11° pctl 1a) |
| **ETH** | $1,891.00 | -3.21% | -0.65% | +20.38% | 45.4% | ↑ ↓ ↓ (1/3 su) | 51.9 (12° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-27 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.5 contro realizzata 30g 31.9 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 51.9 contro realizzata 30g 45.4 (sopra); DVOL al 12° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **34 giorni, 1 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)*
+46
View File
@@ -0,0 +1,46 @@
# Giornale di bordo — 2026-07-28
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,874.50 | +0.26% | -4.00% | +7.35% | 31.7% | ↑ ↓ ↓ (1/3 su) | 37.2 (8° pctl 1a) |
| **ETH** | $1,920.40 | +1.55% | -0.45% | +22.38% | 45.5% | ↑ ↓ ↓ (1/3 su) | 53.0 (14° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-28 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 0.50x (`max_notional_per_asset_frac` 0.50 x 1 asset, letto da `config/live.json`): margine 0.50x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.2 contro realizzata 30g 31.7 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.05 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 53.0 contro realizzata 30g 45.5 (sopra); DVOL al 14° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **35 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-29
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,933.00 | +0.09% | -3.25% | +6.26% | 31.6% | ↑ ↓ ↓ (1/3 su) | 37.5 (11° pctl 1a) |
| **ETH** | $1,909.10 | -0.59% | -1.27% | +18.49% | 45.1% | ↑ ↓ ↓ (1/3 su) | 52.5 (13° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-29 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (21 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 37.5 contro realizzata 30g 31.6 (sopra); DVOL al 11° pctl di un anno; IV-rank espandente 0.06 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.5 contro realizzata 30g 45.1 (sopra); DVOL al 13° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **36 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-30
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,745.50 | +1.27% | -0.48% | +10.59% | 30.0% | ↑ ↓ ↓ (1/3 su) | 35.5 (1° pctl 1a) |
| **ETH** | $1,917.45 | +0.44% | +2.13% | +22.09% | 43.7% | ↑ ↓ ↓ (1/3 su) | 50.7 (8° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-30 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.5 contro realizzata 30g 30.0 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.7 contro realizzata 30g 43.7 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **37 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-07-31
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,837.00 | -2.95% | -1.98% | +4.80% | 31.2% | ↑ ↓ ↓ (1/3 su) | 35.6 (1° pctl 1a) |
| **ETH** | $1,861.50 | -2.92% | +0.05% | +15.77% | 45.0% | ↑ ↓ ↓ (1/3 su) | 50.2 (6° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-07-31 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.6 contro realizzata 30g 31.2 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.2 contro realizzata 30g 45.0 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **38 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-01
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,776.00 | -0.10% | -2.41% | +2.04% | 29.9% | ↑ ↓ ↓ (1/3 su) | 35.5 (1° pctl 1a) |
| **ETH** | $1,843.25 | -0.98% | -1.61% | +8.52% | 41.5% | ↑ ↓ ↓ (1/3 su) | 50.8 (9° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-01 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.5 contro realizzata 30g 29.9 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 50.8 contro realizzata 30g 41.5 (sopra); DVOL al 9° pctl di un anno; IV-rank espandente 0.11 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **39 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-02
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,524.50 | +1.19% | -2.80% | +1.57% | 29.7% | ↑ ↓ ↓ (1/3 su) | 35.0 (0° pctl 1a) |
| **ETH** | $1,883.90 | +2.21% | -3.57% | +7.21% | 40.5% | ↑ ↓ ↓ (1/3 su) | 49.7 (6° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-02 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.0 contro realizzata 30g 29.7 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.7 contro realizzata 30g 40.5 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **40 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-03
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,485.50 | -0.06% | -0.35% | +0.64% | 29.5% | ↑ ↓ ↓ (1/3 su) | 34.8 (0° pctl 1a) |
| **ETH** | $1,859.25 | -1.31% | -1.68% | +4.49% | 40.7% | ↑ ↓ ↓ (1/3 su) | 49.2 (2° pctl 1a) |
## Libro
Equity **$596.92** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-03 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-1.14**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.60%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.8 contro realizzata 30g 29.5 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.2 contro realizzata 30g 40.7 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **41 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-04
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,054.50 | +0.90% | +0.28% | +0.68% | 29.5% | ↑ ↓ ↑ (2/3 su) | 34.0 (0° pctl 1a) |
| **ETH** | $1,868.75 | +0.51% | -2.69% | +4.69% | 40.7% | ↑ ↓ ↑ (2/3 su) | 47.8 (0° pctl 1a) |
## Libro
Equity **$597.37** · nozionale lordo $37 · leva lorda 0.06x · barra dati 2026-08-04 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.162 | flat | $+36 | $+37 | HOLD (a target) |
## P&L
- giorno: **$+0.45** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 1 fill · fee 0.0129
- cumulato dall'arming: **$-0.69**
## 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 ETH $+37 (TP01 +0.162, SKH01 flat).
- `[disaccordo]` ETH: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.162) — e' il meccanismo, non una scelta di oggi.
- `[leva]` Leva lorda **0.06x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.94x.
- `[pnl]` Equity $+0.45 con 1 fill e 0 round-trip chiusi (realizzato $+0.00 netto).
- `[drawdown]` 📌 Equity **-0.53%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.0 contro realizzata 30g 29.5 (sopra); 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 47.8 contro realizzata 30g 40.7 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **42 giorni, 1 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-05
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,607.50 | +0.86% | +1.06% | +0.93% | 29.6% | ↑ ↓ ↓ (1/3 su) | 34.7 (1° pctl 1a) |
| **ETH** | $1,907.45 | +2.07% | -0.09% | +6.04% | 41.2% | ↑ ↓ ↓ (1/3 su) | 48.0 (1° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-05 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$-0.25** di equity (23 letture)
- realizzato: +0.21 netto su 1 round-trip chiusi · 1 fill · fee 0.0130
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $-0.25 con 1 fill e 1 round-trip chiusi (realizzato $+0.21 netto).
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.7 contro realizzata 30g 29.6 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 48.0 contro realizzata 30g 41.2 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **43 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-06
*Scritto 2026-08-23T13:34:14+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,284.50 | -0.50% | -0.71% | +1.52% | 29.4% | ↑ ↓ ↓ (1/3 su) | 35.0 (2° pctl 1a) |
| **ETH** | $1,902.65 | -0.25% | -0.77% | +7.48% | 40.7% | ↑ ↓ ↓ (1/3 su) | 48.0 (1° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-06 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.0 contro realizzata 30g 29.4 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 48.0 contro realizzata 30g 40.7 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **44 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-07
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,884.00 | +0.93% | +3.26% | +4.22% | 28.9% | ↑ ↓ ↓ (1/3 su) | 33.9 (0° pctl 1a) |
| **ETH** | $1,913.50 | +0.57% | +2.79% | +9.83% | 40.2% | ↑ ↓ ↓ (1/3 su) | 47.4 (0° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-07 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 33.9 contro realizzata 30g 28.9 (sopra); 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 47.4 contro realizzata 30g 40.2 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **45 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-08
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,918.50 | +0.05% | +3.41% | +2.72% | 28.4% | ↑ ↓ ↓ (1/3 su) | 34.2 (1° pctl 1a) |
| **ETH** | $1,916.00 | +0.13% | +3.95% | +9.85% | 40.2% | ↑ ↓ ↓ (1/3 su) | 48.1 (2° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-08 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.2 contro realizzata 30g 28.4 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.00 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 48.1 contro realizzata 30g 40.2 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **46 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-09
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,869.00 | -0.08% | +2.12% | +1.14% | 28.0% | ↑ ↓ ↓ (1/3 su) | 34.9 (2° pctl 1a) |
| **ETH** | $1,909.50 | -0.34% | +1.36% | +6.32% | 39.1% | ↑ ↓ ↓ (1/3 su) | 49.1 (4° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-09 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.9 contro realizzata 30g 28.0 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.1 contro realizzata 30g 39.1 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **47 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-10
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,931.50 | -1.45% | +0.70% | +0.20% | 28.4% | ↑ ↓ ↓ (1/3 su) | 36.3 (8° pctl 1a) |
| **ETH** | $1,871.25 | -2.00% | +0.65% | +4.71% | 39.8% | ↑ ↓ ↓ (1/3 su) | 49.7 (8° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-10 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.3 contro realizzata 30g 28.4 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.04 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.7 contro realizzata 30g 39.8 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **48 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-11
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,559.50 | -0.58% | -0.77% | -0.31% | 28.5% | ↓ ↓ ↓ (0/3 su) | 36.1 (8° pctl 1a) |
| **ETH** | $1,881.25 | +0.53% | +0.67% | +4.18% | 39.7% | ↑ ↓ ↓ (1/3 su) | 49.3 (7° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-11 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (23 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 36.1 contro realizzata 30g 28.5 (sopra); 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 49.3 contro realizzata 30g 39.7 (sopra); DVOL al 7° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **49 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-12
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,440.00 | -0.19% | -1.81% | +1.89% | 27.2% | ↑ ↓ ↓ (1/3 su) | 35.8 (6° pctl 1a) |
| **ETH** | $1,878.15 | -0.16% | -1.54% | +5.78% | 39.2% | ↑ ↓ ↓ (1/3 su) | 49.4 (8° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-12 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.8 contro realizzata 30g 27.2 (sopra); DVOL al 6° pctl di un anno; IV-rank espandente 0.03 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 49.4 contro realizzata 30g 39.2 (sopra); DVOL al 8° pctl di un anno; IV-rank espandente 0.10 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **50 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-13
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,453.00 | +0.02% | -1.29% | -2.40% | 22.5% | ↓ ↓ ↓ (0/3 su) | 34.8 (2° pctl 1a) |
| **ETH** | $1,885.15 | +0.37% | -0.92% | -0.24% | 32.6% | ↓ ↓ ↓ (0/3 su) | 48.5 (2° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-13 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.8 contro realizzata 30g 22.5 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 48.5 contro realizzata 30g 32.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.09 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **51 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-14
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,994.50 | -0.72% | -2.91% | -2.69% | 22.6% | ↓ ↓ ↓ (0/3 su) | 34.7 (2° pctl 1a) |
| **ETH** | $1,880.30 | -0.26% | -1.74% | -1.90% | 32.2% | ↓ ↓ ↓ (0/3 su) | 47.2 (0° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-14 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.7 contro realizzata 30g 22.6 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 47.2 contro realizzata 30g 32.2 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **52 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-15
*Scritto 2026-08-23T13:34:15+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $63,033.00 | +0.06% | -2.90% | -1.21% | 22.0% | ↓ ↓ ↓ (0/3 su) | 35.0 (4° pctl 1a) |
| **ETH** | $1,880.85 | +0.03% | -1.83% | +0.93% | 30.7% | ↑ ↓ ↓ (1/3 su) | 47.5 (1° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-15 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.0 contro realizzata 30g 22.0 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 47.5 contro realizzata 30g 30.7 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **53 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-16
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $62,841.50 | -0.30% | -3.13% | -1.66% | 22.0% | ↓ ↓ ↓ (0/3 su) | 35.2 (5° pctl 1a) |
| **ETH** | $1,874.50 | -0.34% | -1.83% | +1.82% | 30.4% | ↑ ↓ ↓ (1/3 su) | 47.5 (1° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-16 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 35.2 contro realizzata 30g 22.0 (sopra); DVOL al 5° pctl di un anno; IV-rank espandente 0.02 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 47.5 contro realizzata 30g 30.4 (sopra); DVOL al 1° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **54 giorni, 2 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)*
+48
View File
@@ -0,0 +1,48 @@
# Giornale di bordo — 2026-08-17
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,487.50 | +2.62% | +0.87% | -0.50% | 23.4% | ↓ ↓ ↓ (0/3 su) | 34.7 (2° pctl 1a) |
| **ETH** | $1,912.35 | +2.02% | +2.20% | +2.72% | 30.9% | ↑ ↓ ↓ (1/3 su) | 46.3 (0° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-17 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.7 contro realizzata 30g 23.4 (sopra); DVOL al 2° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 46.3 contro realizzata 30g 30.9 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +2.62%, **2.1 deviazioni** giornaliere (sd implicita dalla RV30 = 1.22%).
- `[evidenza]` 📌 Campione a oggi: **55 giorni, 2 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)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-18
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $64,693.50 | +0.32% | +1.78% | -0.02% | 23.4% | ↓ ↓ ↓ (0/3 su) | 34.9 (4° pctl 1a) |
| **ETH** | $1,917.10 | +0.25% | +1.91% | +2.43% | 30.9% | ↑ ↓ ↓ (1/3 su) | 46.1 (0° pctl 1a) |
## Libro
Equity **$597.12** · nozionale lordo $0 · leva lorda 0.00x · barra dati 2026-08-18 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+0.00** di equity (22 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 0 fill · fee 0.0000
- cumulato dall'arming: **$-0.94**
## 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]` Libro **flat** su entrambe le gambe: nessun capitale a mercato.
- `[perche_flat]` Motivo, componente per componente — BTC: TP01 a zero (trend giu' o misto) e SKH01 senza breakout; ETH: TP01 a zero (trend giu' o misto) e SKH01 senza breakout. Per una strategia long-flat stare fuori **e'** una decisione.
- `[leva]` Leva lorda **0.00x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 1.00x.
- `[pnl]` Equity $+0.00 **senza operare**: e' mark-to-market sulle posizioni gia' aperte.
- `[drawdown]` 📌 Equity **-0.57%** sotto il picco ($600.53). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 34.9 contro realizzata 30g 23.4 (sopra); DVOL al 4° pctl di un anno; IV-rank espandente 0.01 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 46.1 contro realizzata 30g 30.9 (sopra); DVOL al 0° pctl di un anno; IV-rank espandente 0.07 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **56 giorni, 2 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)*
+49
View File
@@ -0,0 +1,49 @@
# Giornale di bordo — 2026-08-19
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $69,327.00 | +7.16% | +9.28% | +6.30% | 33.6% | ↑ ↓ ↑ (2/3 su) | 37.8 (21° pctl 1a) |
| **ETH** | $2,252.30 | +17.48% | +19.92% | +18.33% | 63.8% | ↑ ↑ ↑ (3/3 su) | 52.5 (21° pctl 1a) |
## Libro
Equity **$610.82** · nozionale lordo $76 · leva lorda 0.12x · barra dati 2026-08-19 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.000 | LONG @ 68,510.0 | $+76 | $+76 | HOLD (a target) |
| **ETH** | +0.000 | flat | $+0 | $+0 | HOLD (a target) |
## P&L
- giorno: **$+13.70** di equity (24 letture)
- realizzato: +10.60 netto su 1 round-trip chiusi · 3 fill · fee 0.0816
- cumulato dall'arming: **$+12.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 $+76 (TP01 +0.000, SKH01 long).
- `[incoerenza]` ⚠️ ETH: tutti e 3 gli orizzonti sono al rialzo ma TP01 e' a zero. Da guardare: non e' lo stato atteso.
- `[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 $+13.70 con 3 fill e 1 round-trip chiusi (realizzato $+10.60 netto).
- `[concentrazione]` 📌 **Il 107% di tutto il P&L cumulato ($+12.76) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[vol]` BTC: implicita 37.8 contro realizzata 30g 33.6 (sopra); DVOL al 21° pctl di un anno; IV-rank espandente 0.08 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.5 contro realizzata 30g 63.8 (sotto); DVOL al 21° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +7.16%, **4.1 deviazioni** giornaliere (sd implicita dalla RV30 = 1.76%).
- `[evento]` 📌 ETH: giornata a +17.48%, **5.2 deviazioni** giornaliere (sd implicita dalla RV30 = 3.34%).
- `[evidenza]` 📌 Campione a oggi: **57 giorni, 3 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)*
+48
View File
@@ -0,0 +1,48 @@
# Giornale di bordo — 2026-08-20
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $73,010.50 | +5.31% | +15.06% | +9.74% | 37.3% | ↑ ↓ ↑ (2/3 su) | 39.4 (33° pctl 1a) |
| **ETH** | $2,326.65 | +3.30% | +23.42% | +20.61% | 64.4% | ↑ ↑ ↑ (3/3 su) | 52.6 (22° pctl 1a) |
## Libro
Equity **$620.63** · nozionale lordo $272 · leva lorda 0.44x · barra dati 2026-08-20 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.199 | LONG @ 68,510.0 | $+124 | $+124 | HOLD (a target) |
| **ETH** | +0.294 | LONG @ 2,251.8 | $+146 | $+148 | HOLD (a target) |
## P&L
- giorno: **$+9.81** di equity (24 letture)
- realizzato: +0.51 netto su 1 round-trip chiusi · 4 fill · fee 0.0698
- cumulato dall'arming: **$+22.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 $+124 (TP01 +0.199, SKH01 long) e ETH $+148 (TP01 +0.294, SKH01 long).
- `[disaccordo]` BTC: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.199) — e' il meccanismo, non una scelta di oggi.
- `[leva]` Leva lorda **0.44x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.56x.
- `[pnl]` Equity $+9.81 con 4 fill e 1 round-trip chiusi (realizzato $+0.51 netto).
- `[concentrazione]` 📌 **Il 104% di tutto il P&L cumulato ($+22.57) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[vol]` BTC: implicita 39.4 contro realizzata 30g 37.3 (sopra); DVOL al 33° pctl di un anno; IV-rank espandente 0.12 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 52.6 contro realizzata 30g 64.4 (sotto); DVOL al 22° pctl di un anno; IV-rank espandente 0.13 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +5.31%, **2.7 deviazioni** giornaliere (sd implicita dalla RV30 = 1.95%).
- `[evidenza]` 📌 Campione a oggi: **58 giorni, 4 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-08-21
*Scritto 2026-08-23T17:43:07+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $78,350.50 | +7.31% | +24.38% | +18.57% | 43.9% | ↑ ↑ ↑ (3/3 su) | 43.3 (57° pctl 1a) |
| **ETH** | $2,517.10 | +8.19% | +33.87% | +30.17% | 69.2% | ↑ ↑ ↑ (3/3 su) | 57.7 (46° pctl 1a) |
## Libro
Equity **$644.00** · nozionale lordo $196 · leva lorda 0.30x · barra dati 2026-08-21 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.176 | LONG @ 75,280.0 | $+123 | $+126 | HOLD (a target) |
| **ETH** | +0.291 | flat | $+70 | $+70 | HOLD (a target) |
## P&L
- giorno: **$+23.37** di equity (24 letture)
- realizzato: +18.90 netto su 6 round-trip chiusi · 5 fill · fee 0.0867
- cumulato dall'arming: **$+45.94**
## 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 $+126 (TP01 +0.176, SKH01 long) e ETH $+70 (TP01 +0.291, SKH01 flat).
- `[leva]` Leva lorda **0.30x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.70x.
- `[pnl]` Equity $+23.37 con 5 fill e 6 round-trip chiusi (realizzato $+18.90 netto).
- `[concentrazione]` 📌 **Il 102% di tutto il P&L cumulato ($+45.94) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[vol]` BTC: implicita 43.3 contro realizzata 30g 43.9 (sotto); DVOL al 57° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 57.7 contro realizzata 30g 69.2 (sotto); DVOL al 46° pctl di un anno; IV-rank espandente 0.20 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evento]` 📌 BTC: giornata a +7.31%, **3.2 deviazioni** giornaliere (sd implicita dalla RV30 = 2.30%).
- `[evento]` 📌 ETH: giornata a +8.19%, **2.3 deviazioni** giornaliere (sd implicita dalla RV30 = 3.62%).
- `[evidenza]` 📌 Campione a oggi: **59 giorni, 10 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-sonnet-5` il 2026-08-23T17:43:32+00:00. Prosa di un modello: puo' sbagliare, e non e' una misura. Controllo sui numeri: **ok**.*
21 agosto — dopo dieci giorni fermi (11-18 agosto, equity ferma a 597.12, zero fill), il libro si è mosso in tre giornate consecutive: il 19 apre BTC ($+76, 1 round-trip), il 20 aggiunge ETH ($+148) e BTC sale a $+124, il 21 chiude entrambe le gambe a target (BTC $+126, ETH $+70) con $+23.37 di equity e $+18.90 realizzato su 6 round-trip. Il cumulato dall'arming passa così a $+45.94, e la pagina segnala che il 102% di quel totale viene dagli ultimi 7 giorni — cioè il periodo precedente era in perdita netta.
Il contesto di mercato spiega perché si sia mosso qualcosa dopo dieci giorni piatti: BTC +7.31% e ETH +8.19% nella giornata, con TSMOM 3/3 al rialzo su entrambi gli orizzonti per entrambi gli asset. La pagina marca la giornata a 3.2 deviazioni per BTC e 2.3 per ETH rispetto alla volatilità realizzata — un movimento ampio, non un drift ordinario. Vale la pena notare che SKH01 è long su BTC (breakout) mentre su ETH resta flat pur con TP01 a convinzione più alta (+0.291 contro +0.176): stesso regime di mercato, due letture diverse a seconda del meccanismo.
La leva resta bassa (0.30x su un tetto di 1.00x) e il feed SKH è fresco (0 minuti). Il campione dichiarato — 59 giorni, 10 round-trip — resta troppo corto perché questi tre giorni buoni dicano qualcosa sull'edge: sono compatibili sia con l'inizio di una fase favorevole al trend sia con normale rumore di un campione piccolo dopo una fase piatta.
## Nota
*(vuota — campo dell'operatore)*
+47
View File
@@ -0,0 +1,47 @@
# Giornale di bordo — 2026-08-22
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $77,103.50 | -1.59% | +22.32% | +18.51% | 43.9% | ↑ ↑ ↑ (3/3 su) | 42.3 (51° pctl 1a) |
| **ETH** | $2,423.10 | -3.73% | +28.83% | +29.06% | 69.8% | ↑ ↑ ↑ (3/3 su) | 57.0 (44° pctl 1a) |
## Libro
Equity **$635.00** · nozionale lordo $327 · leva lorda 0.51x · barra dati 2026-08-22 · 24 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.450 | LONG @ 75,280.0 | $+186 | $+185 | HOLD (a target) |
| **ETH** | +0.277 | LONG @ 2,436.4 | $+145 | $+142 | HOLD (a target) |
## P&L
- giorno: **$-9.00** di equity (24 letture)
- realizzato: +0.00 netto su 0 round-trip chiusi · 2 fill · fee 0.0489
- cumulato dall'arming: **$+36.94**
## 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 $+185 (TP01 +0.450, SKH01 long) e ETH $+142 (TP01 +0.277, SKH01 long).
- `[leva]` Leva lorda **0.51x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.49x.
- `[pnl]` Equity $-9.00 con 2 fill e 0 round-trip chiusi (realizzato $+0.00 netto).
- `[concentrazione]` 📌 **Il 103% di tutto il P&L cumulato ($+36.94) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[drawdown]` 📌 Equity **-1.55%** sotto il picco ($644.98). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 42.3 contro realizzata 30g 43.9 (sotto); DVOL al 51° pctl di un anno; IV-rank espandente 0.17 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 57.0 contro realizzata 30g 69.8 (sotto); DVOL al 44° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **60 giorni, 10 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)*
+48
View File
@@ -0,0 +1,48 @@
# Giornale di bordo — 2026-08-23
*Scritto 2026-08-23T13:34:16+00:00. Numeri misurati; nessuna interpretazione automatica.*
## Mercato
| | chiusura | 1g | 7g | 30g | RV30 ann. | TSMOM 30/90/180 | DVOL |
|---|---|---|---|---|---|---|---|
| **BTC** | $77,254.50 | +0.20% | +22.94% | +20.52% | 43.3% | ↑ ↓ ↑ (2/3 su) | 42.4 (52° pctl 1a) |
| **ETH** | $2,427.20 | +0.17% | +29.49% | +30.46% | 69.6% | ↑ ↑ ↑ (3/3 su) | 57.0 (44° pctl 1a) |
## Libro
Equity **$636.14** · nozionale lordo $258 · leva lorda 0.41x · barra dati 2026-08-23 · 14 giri
| | TP01 | SKH01 | target | posizione | azione |
|---|---|---|---|---|---|
| **BTC** | +0.150 | LONG @ 75,280.0 | $+115 | $+116 | HOLD (a target) |
| **ETH** | +0.271 | LONG @ 2,436.4 | $+144 | $+142 | HOLD (a target) |
## P&L
- giorno: **$+1.14** di equity (14 letture)
- realizzato: +5.55 netto su 2 round-trip chiusi · 1 fill · fee 0.0243
- cumulato dall'arming: **$+38.08**
## Salute
- giri di `book_execute`: 14/14 *(giorno in corso)*
- 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 $+116 (TP01 +0.150, SKH01 long) e ETH $+142 (TP01 +0.271, SKH01 long).
- `[disaccordo]` BTC: gli orizzonti del trend **non concordano** (2/3 al rialzo) e l'esposizione TP01 e' parziale (+0.150) — e' il meccanismo, non una scelta di oggi.
- `[leva]` Leva lorda **0.41x** su un tetto di 1.00x (`max_notional_per_asset_frac` 0.50 x 2 asset, letto da `config/live.json`): margine 0.59x.
- `[pnl]` Equity $+1.14 con 1 fill e 2 round-trip chiusi (realizzato $+5.55 netto).
- `[concentrazione]` 📌 **Il 102% di tutto il P&L cumulato ($+38.08) viene dagli ultimi 7 giorni.** Oltre il 100% perche' **il periodo precedente era in perdita**: senza questi 7 giorni il libro sarebbe sotto. Un risultato concentrato in una finestra non e' un tasso di rendimento: e' un evento.
- `[drawdown]` 📌 Equity **-1.37%** sotto il picco ($644.98). ⚠️ e' un DD su letture ORARIE, non sul minimo intra-giorno.
- `[vol]` BTC: implicita 42.4 contro realizzata 30g 43.3 (sotto); DVOL al 52° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[vol]` ETH: implicita 57.0 contro realizzata 30g 69.6 (sotto); DVOL al 44° pctl di un anno; IV-rank espandente 0.18 sotto la soglia 0.30 del gate di VRP01 (sleeve fermo).
- `[evidenza]` 📌 Campione a oggi: **61 giorni, 12 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)*
+119
View File
@@ -0,0 +1,119 @@
# BRIEF ONDATA 2026-08-22 — ricerca multi-agente, branch `research/wave-0822`
Sei UNO di ~30 agenti. Leggi tutto questo file prima di scrivere codice.
## 0. Obiettivo reale (non e' "trovare uno Sharpe alto")
Il conto vero e' **$635** su Deribit. L'obiettivo dichiarato dall'operatore e' **50 EUR/giorno**.
Misura gia' fatta nel progetto: la rendita perpetua vale ~45-55% del CAGR, quindi 50 EUR/g
richiede **~$258k netti** di capitale. **Nessuna strategia colma quel divario**: il vincolo binding
misurato il 26/07 e' *capitale che entra* + *conto che non sparisce*, non la ricerca.
Percio' un risultato vale in ordine:
1. qualcosa di **ESEGUIBILE a $600-5.000** che aggiunge al libro TP01+SKH01 (raro);
2. qualcosa che alza **P(sopravvivere a una barriera di DD per-conto)** — e' il canale *prop/funded*,
l'unico che moltiplica il nozionale senza possedere il capitale;
3. un **LEAD** onesto con gate pre-registrato e data;
4. una **falsificazione pulita** (chiudere un filone vale quanto aprirlo: si smette di pagarlo).
Un "non funziona" MISURATO e' un risultato accettato e apprezzato. Un PASS non verificato e' un danno.
## 1. Disciplina di macchina — NON NEGOZIABILE
La VPS ha **2 core** e ci gira il libro con **soldi veri**: `cron_book` al minuto **:07**,
`cron_chain` al **:25**, `cron_daily` alle **00:30**.
- Ogni comando python pesante va lanciato con `nice -n 19 timeout 900 uv run python ...`
- **Budget: nessun singolo run > 15 minuti.** Se una griglia non ci sta, RIDUCILA e dichiara che
l'hai ridotta. Meglio 24 celle dichiarate che 700 mai finite.
- Preferisci **1d / 1h**. I 5m esistono (btc/eth) ma una sweep su 5m x griglia = ore: vietata
salvo che sia il cuore della tua ipotesi, e allora su UN asset e UNA finestra.
- **NON toccare**: `config/`, `data/live/`, `data/paper_*`, `data/venue_watch`, `data/fee_watch`,
`src/live/`, `scripts/live/`, `scripts/cron_*.sh`. Sola lettura.
- **NON eseguire** git checkout/commit/push/stash/branch. Il branch e' gia' quello giusto.
- Scrivi **UN SOLO file nuovo**: `scripts/research/r0822_<tuo-slug>.py`. Niente altro in repo.
Output/figure/CSV solo sotto `/tmp/claude-1001/.../scratchpad` o stampati a video.
## 2. Il dato — cosa esiste davvero
Legge con `from src.data.downloader import load_data` oppure `scripts/research/alt/altlib.get()`.
| dato | file | copertura | nota |
|---|---|---|---|
| BTC/ETH OHLCV | `data/raw/{btc,eth}_{5m,15m,1h}.parquet` | 2018/2019 -> oggi | **certificato**, unica verita' (Deribit mainnet) |
| DVOL BTC/ETH | `data/raw/dvol_{btc,eth}.parquet` | 2021-03 -> oggi (1978g) | vol implicita ATM |
| 51 alt Hyperliquid 1d | `data/raw/hl_<sym>_1d.parquet` | 2024 -> oggi (~965g) | serie native, no backfill |
| funding HL 1h | `data/raw/hlfund_<sym>_1h.parquet` | ~2023-05 -> oggi | 19 major |
| ETF/azioni 1d | `data/raw/eq_<sym>_1d.parquet` | fino a 30 anni | SPY/QQQ/IWM/TLT/GLD/HYG certificati; TLT parte 2016 |
| **catena opzioni Deribit** | `data/raw/cb_chain/YYYY-MM-DD.parquet` + `bite_archive.parquet` | **2026-05-01 -> oggi, oraria** | per-strike: bid/ask/mid/iv/delta/gamma/theta/vega/OI/volume/book_depth |
| **snapshot di mercato** | `data/raw/cb_market_snapshots.parquet` | **2026-03-26 -> 2026-07-30** (17.406 righe) | dealer_net_gamma, gamma_flip_level, oi_delta_pct_4h, liquidation_long/short_risk, funding_cross, iv_minus_rv |
| term structure IV | `data/raw/vol_term_{btc,eth}.parquet` | **solo 58 giorni** | iv 7/30/60/90/180d |
Harness catena: `scripts/research/cblib.py` (`load_chain()` unisce archivio + raccolta).
**ATTENZIONE**: catena/snapshot/vol_term hanno **3-5 MESI** di storia. Con 3 mesi non esiste un
hold-out: quei dati possono produrre **al massimo un LEAD con gate pre-registrato**, mai uno sleeve.
Dirlo esplicitamente e' parte del risultato. `load_data("SOL")` = guardrail: SOL sta in `alt_sol_*`
ed e' **escluso per decisione dell'operatore** (22/08) — non riproporlo.
## 3. L'harness onesto — USALO, non riscriverlo
`import sys; sys.path.insert(0, "scripts/research/alt")` poi `import altlib as A`.
- `A.get(asset, tf)` dati · `A.vol_target(...)` · `A.eval_weights(df, target)` metriche nette fee
- `A.eval_weights_smallcap(df, target, capital=600)` -> **haircut reale a $600** (min-order $5)
- `A.candidate_daily(target_fn)` -> serie giornaliera del candidato
- `A.marginal_vs_tp01(cand_daily)` -> ADDS / NEUTRAL / DILUTES / **HEDGE** / **NOISE**
(contiene: robust_oos multicut, has_insample_edge, is_hedge, beats_noise_null)
- `A.study_family_honest(name, factory, grid, tfs)` -> **il gate giusto se hai una GRIGLIA**:
sceglie la cella **in-sample-only** + deflated-Sharpe. `earns_slot_honest` e' il verdetto.
- `A.deflated_sharpe(sr, all_sr, daily)` -> PASS >= 0.95. **Conta TUTTI i trial, al RIALZO.**
- `A.causality_ok(target_fn)` -> nessun look-ahead · `A.day_boundary_robust(...)` per segnali calendario
- `A.implausible_sharpe(daily, n_trades, n_losing)` -> Sharpe troppo bello = rischio FUORI dal dataset
- `A.anchor_luck_band(fn_by_offset, offsets)` -> la stima onesta e' la **MEDIANA**, non l'ancora 00:00
- `A.anchor_luck_delta(A_by_off, B_by_off, offsets)` -> per confrontare due varianti:
**mediana delle DIFFERENZE APPAIATE**, mai differenza delle mediane.
## 4. I modi in cui questo progetto si e' gia' ingannato (li conosciamo per nome)
Se il tuo risultato ha una di queste forme, e' quasi certamente quello — provalo prima di crederci.
1. **Null del de-levering** (5 occorrenze): "meno drawdown" e' quasi sempre solo meno leva.
Test obbligatorio su OGNI claim di DD: esiste `k<1` che, applicato al baseline, da' lo STESSO
DD con Sharpe **migliore**? Se si', il tuo overlay e' inutile.
2. **TP01 travestito**: corr al baseline > ~0.6 = stai ricomprando il trend. `marginal_vs_tp01`.
3. **Selezione sull'hold-out**: scegliere la cella col miglior OOS. Usa `select_cell_insample`.
4. **Fortuna d'ancora**: l'ora/il giorno di ribilanciamento e' un max-of-k non dichiarato.
5. **Sharpe implausibile**: 0 perdite su N non e' "senza rischio" (regola del tre: fino a 3/N).
6. **Hedge travestito da alpha**: paga solo quando TP01 e' debole -> `is_hedge`.
7. **Il win-rate e' un KNOB**: WR ~ 1/(1+RR). Converti ogni claim "X% win" in **expectancy R netta fee**.
8. **Una riga presente non e' un dato presente**: conta cio' che e' QUOTATO/ATTIVO, non le righe.
9. **Barre attive vs calendario**: uno sleeve settimanale su griglia giornaliera e' 94% zeri.
10. **Confronto appaiato per INGRESSO**, mai allineato sull'uscita (e' quella che la variante cambia).
## 5. Cosa e' GIA' MORTO — non riaprirlo senza un meccanismo nuovo
mean-reversion/fade · trend 5m-15m (fee) · calendario in ogni forma (weekend 375 trial, expiry,
stagionalita', event-clock, ora-del-giorno) · funding (carry CC01, time-series, cross-sectional:
chiuso su 3 lati) · macro regime gate (ridondante col trend) · gamma scalping long-vol · CRT/ICT/
Elliott/Fibonacci/Albimarini · espansione universo XS01 · meta-allocazione dinamica · trend
multi-asset (MAT01) · overlay DD su VRP01 (5/5 refutati) · SOL terza gamba (deciso 22/08) ·
breadth alt (rivedibile solo con 1-2 anni di storia HL in piu').
Il **soffitto strutturale BTC/ETH-direzionale e' ~1.3 di Sharpe**: superato una sola volta, e
cambiando meccanismo (cross-sectional).
## 6. Il libro di oggi — il tuo termine di paragone
LIVE su Deribit: **TP01 75% + SKH01 25%**, nettati sullo stesso strumento (USDC linear), cron orario,
cap $318/asset, min order $5, disaster-SL -30%, fee reale **3,5 bps/lato**.
Book di ricerca a 5 sleeve (TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20):
Sharpe FULL **1.95** e HOLD **1.54** (stime de-luckate; i canonici 2.22/2.36 sono fortuna d'ancora),
maxDD ~6.9%. GTAA01 **non e' deployabile** (PRIIPs) e XS01 serve ~$20k, XSR01 ~$5k.
Un candidato si giudica **marginalmente vs questo**, mai in assoluto.
## 7. Contratto di consegna (obbligatorio)
Consegni **testo strutturato**, non un racconto. Alla fine del tuo lavoro riporta ESATTAMENTE:
- `IPOTESI`: cosa hai testato, in una frase, e **cosa ti aspettavi PRIMA di misurare**.
- `GRIGLIA DICHIARATA`: quante celle/trial hai valutato in TOTALE (conta al rialzo, incluse le
varianti provate e scartate). Se hai ridotto la griglia per il budget, dillo.
- `NUMERI`: Sharpe FULL / hold-out / maxDD / CAGR, **netti fee**, + haircut a $600 se pertinente.
- `GATE`: esito di ciascun gate che hai potuto far girare (marginal, DSR, causality, anchor,
de-levering null, implausible). Un gate non girato si dichiara "non girato", non si omette.
- `VERDETTO`: uno fra `SCARTATO` / `LEAD` / `CANDIDATO` — e **la ragione in una riga**.
`CANDIDATO` solo se: `earns_slot`/`marginal=ADDS` **E** DSR>=0.95 **E** null-de-levering superato
**E** eseguibile al capitale dichiarato. Altrimenti e' `LEAD` (con gate e data) o `SCARTATO`.
- `COSA MI SMENTIREBBE`: l'osservazione che falsificherebbe il tuo risultato.
- `SCRIPT`: il path del file che hai scritto (deve girare da solo: `uv run python <path>`).
Se non hai potuto misurare, il verdetto e' `SCARTATO (non misurabile)` col motivo. **Non inventare
numeri, non estrapolare da un run non finito, non riportare un risultato di un run che hai ucciso.**
## 8. RAM — 7 GB totali, ~3 GB liberi, e siete in tanti
Non caricare i `*_5m.parquet` di piu' di un asset alla volta, e taglia la finestra
(`pd.read_parquet(f, columns=[...])` + slice temporale) prima di elaborare. Se ti serve la catena
opzioni, filtra per asset/scadenza in lettura: `bite_archive.parquet` da solo e' 35 MB su disco ma
esplode in memoria. Un agente che fa OOM uccide anche il lavoro degli altri.
File diff suppressed because it is too large Load Diff
+618
View File
@@ -0,0 +1,618 @@
# SPEC — la CHIAVE DI SCALA del libro live (`book_scale_k`)
**Filone SCALE-SPEC, ondata 2026-08-22, branch `research/wave-0822`.**
**Questo documento e' una SPECIFICA. Non e' stata implementata: `src/`, `config/`, `scripts/live/`,
`scripts/cron_*.sh` e `tests/` non sono stati toccati.** Il prototipo che dimostra la meccanica e'
`scripts/research/r0822e_scale_proto.py` (isolato, non importato da niente, non scrive nulla; la
sua sezione (0) verifica entrambe le cose a runtime invece di dichiararle).
Nasce da `RESULTS-0822.md` §33 (WORST-DAY), che ha misurato che il gradino di leva e' l'unica leva
grande rimasta e che **C6 (processo) FALLISCE**: la regola del progetto — *«ogni cambio di SCALA
passa dal cap di config, non da `target_vol`»* — **non e' implementabile come scritta**, perche' il
cap e' un CLAMP e non un moltiplicatore.
---
## 0. IL RISCHIO CHE QUESTA SPECIFICA DEVE RENDERE IMPOSSIBILE
> **Una chiave di scala e' esattamente il tipo di parametro che si alza «solo un po'» dopo un mese
> buono.** E' un numero, sta in un file di config, non richiede di capire niente per cambiarlo, e il
> suo effetto e' immediato e piacevole.
Il progetto ha regole contro questa forma di deriva su ogni altro asse — `weights_tilt_null` per i
pesi, gate pre-registrati per i candidati, `select_cell_insample` per le griglie, `anchor_luck_band`
per le ancore — e **nessuna per la scala**. Non per dimenticanza: **per aritmetica.**
Lo **Sharpe e' invariante alla scala**. Misurato nel prototipo (sez. 7a), sulla serie vera del libro:
| k | Sharpe | drift | vol | maxDD | g(k) esatto |
|---|---|---|---|---|---|
| 1,00 | **1,5051** | 17,11% | 11,37% | −12,17% | 17,90% |
| 1,25 | **1,5051** | 21,39% | 14,21% | −15,01% | 22,61% |
| 2,00 | **1,5051** | 34,22% | 22,74% | −23,10% | 37,24% |
`deflated_sharpe` e `marginal_vs_tp01` leggono la prima colonna. **Non falliscono: non vedono la
variabile.** Un raddoppio della leva passa ogni gate esistente del progetto senza far scattare nulla,
e produce lo stesso identico numero di sintesi che il progetto usa per giudicare tutto il resto.
Da qui discendono i tre vincoli non negoziabili del disegno:
1. **il tetto sta nel CODICE, non in config** (una config puo' essere modificata da chiunque, in
qualunque momento, senza review; una costante di codice no);
2. **il tetto sta sul PRODOTTO, non sulla chiave** (`frac` e `scala` moltiplicano: un tetto sulla
sola chiave lascia aperta la porta accanto — vedi §4.2, tabella);
3. **il gate e' uno SCRIPT che stampa PASSA/FALLISCE, non un giudizio** (§6), e c'e' un
**sorvegliante giornaliero** che se ne accorge *dopo il fatto* se qualcuno lo salta (§6.4).
⚠️ **Correzione dovuta al mandato di questo filone.** Il gradino non vale quello che dice la frase
che circola. `RESULTS-0822.md` §2 misura **1,00x → 1,25x = 14,7a → 12,9a**, e **1,00x → 1,50x =
14,7a → 11,6a**; il «~€300/mese di versamenti equivalenti» appartiene al gradino **1,50x**.
Ricalcolato sulla tabella netta pubblicata (`r0807_piano_netto`: €500/m → 14,7a, €800/m → 11,4a,
interpolazione lineare **dichiarata**):
| gradino | anni al capitale-rendita | versamento mensile equivalente | delta |
|---|---|---|---|
| 1,00x (oggi) | 14,7a | €500/m | — |
| **1,25x** | **12,9a** | ~€664/m | **+€164/m** |
| 1,50x | 11,6a | ~€782/m | +€282/m |
L'interpolazione riproduce il «~€300/mese» pubblicato per 1,50x (€282) → è un controllo, non solo
un conto. **Questa specifica autorizza il gradino a 1,25x e vale ~€165/mese, non ~€300.**
---
## 1. COSA SPECIFICA, E COSA NON FA
**Specifica:** dove va la chiave, in che ordine si applica rispetto al cap, quali guardie servono,
quali test devono esistere **prima** della modifica, quale gate deve essere superato per alzarla e
con quale cadenza si sorveglia.
**NON fa:** non autorizza il gradino, non implementa niente, non tocca `config/live.json`. Con la
chiave assente o `= 1.0` il libro e' **bit-exact quello di oggi** (§5, test T7): la modifica e'
inerte finche' qualcuno non scrive un numero, e quel numero passa dal gate del §6.
---
## 2. DOVE VA LA CHIAVE, ESATTAMENTE
### 2.1 La catena, letta dal sorgente vero
Il prototipo la stampa leggendo i file (sez. 2), cosi' le righe qui sotto sono verificabili e non
citate a memoria:
| stadio | file:riga | cosa produce |
|---|---|---|
| segnale TP01 | `src/live/shadow.py:119` `targets = {a: tp.current_target(dfs[a])...}` | una **frazione** ≥0, long-flat |
| segnale SKH01 | `src/live/book.py:217` `skh = _skyhook_positions(...)` → `:228` `sign = _skh_sign(st)` | un **segno** +1/−1/0 |
| cap per-asset | `src/live/book.py:107` `def _cap(...)` → `:194` `cap = _cap(...)` | il **clamp**, legato al watermark |
| **TARGET NETTO** | **`src/live/book.py:138` `def book_net_target(...)`** → **`:229` `net = book_net_target(tp_frac, sign, equity, cap)`** | **← QUI VA LA SCALA** |
| ordine | `src/live/book.py:153` `def build_book_order(...)` → `:232` | un **delta** verso il target |
| invio | `scripts/live/book_execute.py:208` `trader.rebalance_signed(inst, net, mark, ...)` | il fill |
### 2.2 La firma proposta
```python
# src/live/book.py
def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float,
weight: float = WEIGHT, scala: float = 1.0) -> float:
raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign))
return scala * max(-cap, min(cap, raw)) # <-- la scala e' FUORI dal clamp: vedi §3
```
più, accanto a `_cap()`, un lettore dedicato:
```python
LEVA_LORDA_MAX = 1.25 # COSTANTE DI CODICE, non di config. Vedi §4.1.
SCALA_LADDER = (1.00, 1.25) # i soli valori esprimibili. Vedi §4.1 e §6.
def _scala(real_equity, eq_fallback) -> float:
"""Scala del libro. 1.0 (= il libro di oggi) in ogni stato degradato: vedi §4.3."""
if real_equity is None or eq_fallback:
return 1.0
... # legge config, valida contro LEVA_LORDA_MAX e SCALA_LADDER, ALZA se fuori (§4.1)
```
e in `book_report`, una riga sotto `cap = _cap(...)`:
```python
scala = _scala(sh.get("real_equity"), sh.get("eq_fallback"))
...
net = book_net_target(tp_frac, sign, equity, cap, scala=scala)
```
`book_report` deve **esporre `scala` nel dict di ritorno** accanto a `cap_per_asset`, e
`book_execute` deve **stamparla nella riga «sizing base»**: un parametro che agisce sul nozionale e
non compare nel report e' la forma esatta del difetto che questo progetto ha gia' pagato tre volte
(docstring che dichiarano una causalita' che il codice non ha).
### 2.3 Perche' **li'** e non altrove — e la dimostrazione
`book_net_target` e' **l'unico punto della catena in cui le due gambe si incontrano con una TAGLIA**:
sopra di lei TP01 da' una frazione e SKH01 da' **solo un segno**; sotto di lei si calcolano delta e
ordini. Percio' e' l'unico punto in cui un moltiplicatore e' un puro cambio di scala.
Il prototipo (sez. 3) lo misura sulla funzione di produzione stessa — la copia e' **bit-exact**
(`max|copia − produzione| = 0.00e+00` su 480 combinazioni):
| k | solo TP01 (tp=1, sg=0) | solo SKH01 (tp=0, sg=1) | rapporto TP/SKH | peso implicito TP01 |
|---|---|---|---|---|
| 1,00 | 2.267,62 | 755,88 | **3,000000** | **0,750000** |
| 1,25 | 2.834,53 | 944,84 | **3,000000** | **0,750000** |
| 2,00 | 4.535,25 | 1.511,75 | **3,000000** | **0,750000** |
`max|target(k) − k·target(1)| = 0.00e+00` su 90 combinazioni. **Il mix 75/25 e' invariante a ogni k:
e' una scala, non un tilt.**
Le alternative, e perche' cadono:
| dove | perche' NO |
|---|---|
| `TrendPortfolio.target_vol` | scala il **75%** del libro e lascia fermo il 25% → e' anche un **tilt di pesi**, e **fallisce `weights_tilt_null`**. Misurato nel prototipo come **controllo positivo**: a k=1,25 il peso implicito di TP01 passa da 0,750 a **0,789**. |
| `_skyhook_positions` | non c'e' niente da scalare: produce un **segno**. |
| `WEIGHT` (=0,5) | e' lo **split BTC/ETH**, ed e' **condiviso** con `shadow_report`/`build_rebalance_order` (`shadow.py:144`) e con la dashboard, cioe' con il percorso **TP01-only**: cambiarlo cambierebbe anche un altro libro. E confonderebbe in un solo numero «quanti asset» e «quanta leva». |
| `W_TP01` / `W_SKH` | sono il **rapporto** 75/25 e sono legati per test ai pesi del backtest (`test_weights_match_backtest_sleeves` asserisce `W_TP01+W_SKH == 1.0` e la parita' con `deribit_book_sleeves`). Moltiplicarli entrambi romperebbe quella parita' e renderebbe impossibile distinguere scala e mix. |
| `build_book_order` | scalerebbe il **delta**, non il target: cambia la **velocita' di convergenza**, non la taglia, e il reconciler litigherebbe con se' stesso a ogni giro. |
| dentro `DeribitTrader` | il report, il dry-run e `book_executions.jsonl` stamperebbero un target **diverso** da quello eseguito. |
---
## 3. L'INTERAZIONE COL CAP — il punto sottile
### 3.1 Il fatto geometrico
Il cap e' un **clamp**: `net = clamp(0.5·E·(0,75·tp + 0,25·sg), ±cap)` con `cap = frac·E = 0,50·E`.
A piena convinzione (`tp=1, sg=+1`) il grezzo vale **esattamente** `0,5·E·1,0 = 0,50·E = cap`:
**il clamp e' tangente al libro proprio nei giorni di massima esposizione.**
### 3.2 I due ordini, misurati (prototipo sez. 4, su 5.648 giorni-asset veri)
| k | ordine | clampati | quota | k_eff medio | **k_eff a piena convinzione** | lordo max |
|---|---|---|---|---|---|---|
| 1,25 | **PRIMA** del clamp | 32 | 0,57% | 1,2487 | **1,0000** | 1,000 |
| 1,25 | **DOPO** il clamp | 0 | 0,00% | **1,2500** | **1,2500** | 1,250 |
| 2,00 | PRIMA | 793 | 14,04% | 1,9235 | **1,0000** | 1,000 |
| 2,00 | DOPO | 0 | 0,00% | **2,0000** | **2,0000** | 2,000 |
**L'ordine corretto e' DOPO il clamp**, e la ragione non e' di comodo:
- **PRIMA**: il clamp mangia la scala **proprio nei giorni di massima convinzione** (k_eff = 1,00 a
piena convinzione, a ogni k). Il risultato non e' «il libro a leva k»: e' un libro che alza i
giorni piccoli e lascia fermi i grandi, cioe' un **appiattimento del profilo di convinzione**. E'
un cambio di **FORMA** travestito da cambio di taglia — e allora **la curva g(k) di
`r0822_growth_policy` / WORST-DAY non descrive quel libro**, perche' quella curva e' calcolata su
`k·r`, cioe' sull'ordine DOPO. Autorizzare il gradino con una curva e implementarne un'altra e'
il modo piu' silenzioso di sbagliare che questa specifica possa produrre.
- **DOPO**: `k_eff = k` esattamente, in ogni giorno e in ogni stato del segnale. E' l'unico ordine
per cui «leva k» significa la stessa cosa nel codice e nel backtest.
### 3.3 Cosa implica per il SIGNIFICATO del cap — e va detto, perche' e' il prezzo
Con l'ordine DOPO, **il cap smette di essere il tetto assoluto del nozionale**. Diventa il tetto
del **libro unitario**: limita la *forma* (nessun asset oltre `frac·E` di esposizione unitaria),
mentre la *taglia* la decide la scala. Il lordo massimo passa da `n_asset·frac` a
**`n_asset·frac·scala`** = 1,250x l'equity a k=1,25.
E il cap **non smette di fare le altre due cose che fa oggi**, che sono quelle che contano davvero:
1. resta **proporzionale all'equity** (`cap = equity·frac` sul percorso fidato) → un deposito non
strozza il libro e un prelievo lo rimpicciolisce da solo;
2. resta **legato al watermark** sul percorso di fallback (`min(fisso, watermark·frac)`) → il caso
che il 26/07 valeva «3,35x di leva su un conto da $597» resta chiuso (§4.3).
⚠️ **Cio' che il cap NON puo' piu' fare da solo e' essere la guardia di sicurezza sulla leva
lorda.** Quella guardia va ricostruita esplicitamente, ed e' il §4.
### 3.4 Il test che si rompe — e **deve** rompersi
`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`:
```python
leva_max = frac * len(FS.ASSETS)
assert leva_max <= 1.0, ("leva massima ...: la liquidation fee 1% torna rilevante, "
"rivedere r0726_fee_sensitivity e il diario 2026-07-26-fee-deribit.md")
```
**Si rompe? No — e questo e' il punto.** Con `frac` invariato a 0,50 il test continua a **passare**,
perche' misura `frac · n_asset` mentre la grandezza vera diventa `frac · n_asset · scala`.
**Non si rompe, e smette di controllare cio' che era stato scritto per controllare.**
E' esattamente la forma di difetto che il progetto ha catturato **tre volte in un giorno** il
2026-08-21: *un controllo puntato su una configurazione diversa da quella che gira passa sempre, e
non sta controllando niente* (`fee_watch` sugli inverse, `venue_watch` con bitfinex, i test di
`book_live` a libro flat).
**Quindi va sostituito, non aggiustato**, e la sostituzione e' una coppia:
- **T2** (§5) — `test_la_leva_lorda_da_config_resta_sotto_il_tetto`: asserisce
`len(ASSETS) * frac * scala <= LEVA_LORDA_MAX`, cioe' **il prodotto**, e importa la scala **dallo
stesso posto da cui la importa la produzione** (regola del 21/08: *un sorvegliante deve DERIVARE
il proprio bersaglio dal codice sorvegliato, mai ridichiararlo*).
- **prerequisito del gate** (§6): `r0726_fee_sensitivity` va **rifatto** al nuovo lordo, perche' la
sua conclusione *«liquidation fee 1% irrilevante»* era esplicitamente condizionata a nozionale
lordo ≤1x. Al gradino il lordo e' 1,25x → una liquidazione costerebbe **1,25%** dell'equity, non
1,00%. Non e' un problema; e' un numero che va **ricalcolato e ripubblicato**, non ereditato.
Il vecchio test va **cancellato con una nota** che dice perche' (e rimandi a questo documento), non
modificato in silenzio: un test di decisione che cambia soglia senza lasciare traccia e' un test di
decisione perso.
---
## 4. LE GUARDIE
### 4.1 G1 — il tetto: **sul PRODOTTO, in CODICE, e non silenzioso**
```python
LEVA_LORDA_MAX = 1.25 # src/live/book.py — costante di CODICE
SCALA_LADDER = (1.00, 1.25)
```
Tre proprieta', tutte necessarie:
**(a) Sul prodotto.** `frac` e `scala` **moltiplicano**. Un tetto sulla sola chiave lascia aperta la
porta accanto (prototipo sez. 6):
| frac | scala | leva lorda | tetto «scala ≤ 1,25» | **tetto sul PRODOTTO** |
|---|---|---|---|---|
| 0,500 | 1,25 | 1,250 | passa | passa |
| **0,625** | **1,25** | **1,562** | **passa** | **BLOCCA** |
| **0,750** | **1,25** | **1,875** | **passa** | **BLOCCA** |
| 0,500 | 2,00 | 2,000 | BLOCCA | BLOCCA |
Ed e' un rischio **concreto, non teorico**: WORST-DAY ha appena pubblicato che «alzare `frac` da
0,50 a 0,625 non produce k=1,25», il che rende `frac` un candidato naturale al prossimo ritocco —
salvo che, **combinato con la scala, lo produce eccome**.
**(b) In codice.** Il tetto di config sarebbe modificabile dalla stessa mano, nello stesso file, nel
momento in cui si modifica la chiave: sarebbe un lucchetto con la chiave attaccata. Con
`LEVA_LORDA_MAX` in `src/live/book.py`, il gradino **1,25 → 1,50 richiede una modifica di codice**,
quindi una review, quindi un gate. **Il tetto e' basso apposta: e' il meccanismo del cricchetto, non
un'opinione sulla leva massima sopportabile** (che dalla §4.5 sarebbe 1,67x).
**(c) Non silenzioso.** Una scala fuori tetto (o fuori `SCALA_LADDER`) **NON viene tagliata al
tetto**: `book_execute` **si ferma, non invia ordini, e allerta**. Un clamp silenzioso farebbe
girare una config che dichiara un numero e un libro che ne esegue un altro — la stessa classe di
difetto del §3.4. `SCALA_LADDER` esiste perche' **una scala a gradini rende inesprimibile «solo un
po'»**: 1,05 non e' un valore prudente, e' un valore fuori scaletta, e viene rifiutato.
### 4.2 G2 — l'invariante che lega TRE chiavi
```python
assert len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50
```
Vedi §4.5: e' la guardia che morde per prima, e cattura chi allarga il disaster-SL.
### 4.3 G3/G4 — il watermark e l'equity non leggibile
**Regola proposta: la scala vive SOLO sul percorso fidato.**
```python
scala_effettiva = scala se l'equity REALE e' leggibile, altrimenti 1.00
```
Motivo: **una scala e' una decisione di rischio presa conoscendo il conto; quando non si sa quanto
vale il conto, non si prende.** Cosi' «il fallback non e' piu' permissivo» e' vero **per
costruzione**, non per aritmetica — e non dipende dal fatto che il conto degli invarianti torni.
Misurato (prototipo sez. 6-bis; watermark reale letto da disco, $634,996; copia di `_cap`
verificata bit-exact contro la produzione):
| stato | cap | scala eff. | lordo max | **/ equity vera** | tetto |
|---|---|---|---|---|---|
| normale (equity reale letta) | 317,50 | **1,25** | 793,74 | **1,250x** | si |
| online ma equity illeggibile | 317,50 | **1,00** | 635,00 | **1,000x** | si |
| watermark assente (1° avvio) | 300,00 (`CAP_UNKNOWN_USD`) | **1,00** | 600,00 | 0,945x | si |
| watermark vecchio e basso | 50,00 | **1,00** | 100,00 | 0,157x | si |
| dopo un deposito a $6.047 | 3.023,50 | **1,25** | 7.558,75 | 1,250x | si |
**In nessuno stato degradato la leva supera quella dello stato normale**, e il caso «watermark
assente» ha **due protezioni indipendenti** (cap $300 **e** scala 1,00), non una sola.
⚠️ **Prezzo dichiarato.** Nei giri in cui l'equity non e' leggibile il libro gira a k=1 e al giro
dopo torna a k → un ribilancio in piu' per oscillazione, di `(k−1)·frac·E` per asset = **~$79 a
testa** sul conto di oggi, cioe' **~$0,056** di fee a 3,5 bps. **Frequenza misurata** su
`logs/cron_book.log`: **0 giri su 1.442 (0,00%)**; i 21 giri «conto non leggibile» sono OFFLINE, e
li' il libro non manda ordini affatto, quindi la scala non si pone. → **il prezzo esiste, e' noto,
ed e' nullo sul campione osservato.**
⚠️ **E questo e' anche il limite dichiarato della regola:** se un giorno il ramo di fallback
diventasse frequente, il libro girerebbe a una leva **mista** e la curva `g(k)` smetterebbe di
descriverlo. → **il sorvegliante del §6.4 deve riportare quella frequenza**, e se supera il **2%
dei giri in 90 giorni** la regola va ridiscussa (soglia dichiarata qui, prima di vedere il caso).
**Alternativa scartata:** congelare l'ultimo target scalato durante il fallback. Aggiunge **stato**
a un percorso con soldi veri — cioe' un nuovo modo di essere sbagliati — per risparmiare $0,056.
### 4.4 G5 — il `min_order_usd` non e' un problema, e va detto
La scala **aumenta** i delta, quindi allontana dal pavimento $5 invece di avvicinarcisi. Nessuna
azione. (Lo si dice perche' «non e' un problema» misurato vale quanto un problema trovato: evita
che la domanda torni.)
### 4.5 G6 — il disaster-SL: **la guardia che morde per prima, e non era stata calcolata**
Il disaster-SL e' un movimento di **prezzo** (−30% sul mark), non una perdita in equity. L'impatto
in equity e' quel movimento **moltiplicato per il lordo**, quindi **cresce con k anche se
`disaster_sl_pct` non cambia**. Prototipo sez. 6, tutte le righe scalate dai numeri **replicati**
di WORST-DAY (§7):
| k | lordo max | A) peggior giorno possibile | **B) costo disaster-SL** | C) x_liq | min intra·k | margine C |
|---|---|---|---|---|---|---|
| 1,00 | 0,864 | 14,32% | **30,00%** | 115,8% | 7,33% | 15,8x |
| **1,25** | **1,081** | **17,90%** | **37,50%** | **92,5%** | **9,16%** | **10,1x** |
| 1,50 | 1,297 | 21,48% | 45,00% | 76,9% | 11,00% | 7,0x |
| 2,00 | 1,729 | 28,64% | **60,00%** ← sfonda | 57,4% | 14,66% | 3,9x |
📌 **La guardia che morde per prima NON e' il peggior giorno possibile, e' il disaster-SL:**
- da (A): `k ≤ 50% / 14,32%` = **3,49x**
- da (B): `k ≤ 50% / (2 · 0,50 · 30%)` = **1,67x** ← **2,1x piu' stringente**
A k=1,25 il margine di (A) e' **2,8x** (il numero che WORST-DAY pubblica come C1) mentre quello di
(B) e' **1,33x**. **Il criterio C1 di WORST-DAY guardava la grandezza sbagliata**: guardava il
peggior giorno del *segnale*, non il costo del dispositivo di emergenza che il libro ha davvero
armato sul conto. Da qui l'invariante G2 (§4.2), che con `sl_pct=30%` e `frac=0,50` da'
**scala ≤ 1,67x** — e che scatta se qualcuno allarga il disaster-SL.
**Decisione: `disaster_sl_pct` NON si tocca.** Stringerlo a 24% per tenere invariante l'impatto in
equity lo trasformerebbe da dispositivo di catastrofe in **stop di strategia**: verrebbe toccato dal
rumore, e il progetto ha gia' misurato cosa succede quando uno stop stretto incontra un gap (SKH01,
`sl 2% modellato → −11/−23% realizzato`). Si accetta il 37,5% e **lo si scrive**.
⚠️ **Osservazione da LETTURA DEL CODICE, non misurata, e va citata cosi'.**
`src/live/execution.py:196-223` **ri-ancora** il disaster-SL al mark a ogni giro: se lo stop esistente
dista piu' del **5%** relativo da `mark·(1−sl_pct)`, viene **cancellato e ripiazzato**. Per un long
questo significa che lo stop **segue il prezzo anche in discesa**: il −30% e' un −30% **rotolante
dall'ultimo piazzamento**, non un pavimento assoluto sul trade. Quindi **B e' il costo di UN
episodio**, e un ribasso lento puo' produrne piu' d'uno.
Evidenza circostanziale, non prova: nel log ci sono **15 `placed` contro 259 `ok`** su asset con
posizione aperta — coerente con qualche ri-ancoraggio, ma i `placed` includono anche le prime
aperture e **non sono separabili dal log**. → **e' una proprieta' di oggi che il gradino amplifica,
non una che il gradino crea**, ed e' un follow-up con una domanda precisa: *quante volte il bracket
e' stato ri-piazzato mentre la posizione restava aperta?* (misurabile aggiungendo `pos_prima` al log,
non ricostruibile da quello attuale).
---
## 5. I TEST CHE DEVONO ESISTERE **PRIMA** DELLA MODIFICA
Scritti come specifiche eseguibili: **nome + cosa asserisce + il caso che deve far fallire.**
File proposto: `tests/test_book_scale.py` (piu' le modifiche dichiarate a
`tests/test_fee_sensitivity.py`).
| # | test | asserisce | **deve fallire se** |
|---|---|---|---|
| **T1** | `test_la_scala_e_identica_sui_due_sleeve` | su una **griglia di k** (1,00/1,10/1,25/1,50/2,00) × tp_frac × segno, con cap disattivato: `target(k) == k·target(1)` **e** `target(tp=1,sg=0)/target(tp=0,sg=1) == W_TP01/W_SKH` costante | qualcuno applica la scala a **una sola** gamba (o dentro `TrendPortfolio`) |
| **T1b** | `test_il_test_di_simmetria_smaschera_una_scala_asimmetrica` | **CONTROLLO POSITIVO**: la stessa asserzione, applicata a un'implementazione asimmetrica scritta nel test, **fallisce** | il test T1 gira **solo a k=1** (dove le due implementazioni **coincidono**: potenza **zero** — misurato nel prototipo) |
| **T2** | `test_la_leva_lorda_da_config_resta_sotto_il_tetto` | `len(ASSETS) * frac * scala <= LEVA_LORDA_MAX`, con `frac` e `scala` **letti da `config/live.json`** e `LEVA_LORDA_MAX`/`ASSETS` **importati dal modulo di produzione** | qualcuno alza `frac`, o la scala, **o entrambe di poco** |
| **T3** | `test_l_invariante_disaster_sl_tiene_meta_equity` | `len(ASSETS) * frac * scala * disaster_sl_pct <= 0.50` | qualcuno allarga `disaster_sl_pct` **o** alza la scala fino a 1,67x |
| **T4** | `test_il_fallback_non_e_piu_permissivo_del_percorso_fidato` | in **ogni** stato degradato (equity illeggibile / watermark assente / watermark vecchio) il lordo massimo **/ equity vera** ≤ quello dello stato normale | qualcuno applica la scala anche in fallback, o slega il cap dal watermark |
| **T5** | `test_il_cap_continua_a_limitare_cio_che_limitava` | il target per-asset non supera mai `cap·scala`; e il clamp **morde ancora** sulle stesse osservazioni di prima (le 3 su 5.650) | qualcuno inverte l'ordine (§3.2) o toglie il clamp «tanto c'e' la scala» |
| **T6** | `test_una_scala_fuori_tetto_ferma_l_esecuzione_e_non_viene_tagliata` | con `scala` fuori `SCALA_LADDER` o fuori tetto: `book_execute` **non invia**, allerta, e il valore **non** viene silenziosamente clampato | qualcuno «rende robusto» il lettore con un `min(scala, LEVA_LORDA_MAX/…)` |
| **T7** | `test_senza_la_chiave_il_libro_e_quello_di_oggi` | con chiave assente **o** `= 1.0`, `book_net_target` e' **bit-exact** alla versione attuale su una griglia (`max|diff| == 0.0`) | la modifica cambia qualcosa **prima** che qualcuno decida di cambiarlo |
| **T8** | `test_il_report_e_il_log_dichiarano_la_scala` | `book_report()['scala']` esiste e `book_execute` la stampa | un parametro che muove il nozionale resta invisibile nel report |
| **T9** | `test_la_scala_e_derivata_dal_codice_non_ridichiarata` | il sorvegliante e i test importano `LEVA_LORDA_MAX`, `ASSETS`, `SCALA_LADDER` **da `src/live/book`** e non li riscrivono | due liste in due file (regola del 21/08, 4 occorrenze) |
| **T10** | `test_il_sorvegliante_segnala_una_scala_non_autorizzata` | **CONTROLLO POSITIVO** del §6.4: con una config che dichiara una scala assente dal giornale, il sorvegliante **allerta**; con config e giornale concordi, **tace** | il sorvegliante e' tarato per non segnalare mai (regola del 26/07: *un rilevatore tarato per non segnalare e' indistinguibile da uno rotto*) |
| **T11** | `test_il_vecchio_test_di_leva_e_stato_sostituito_non_rilassato` | `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **non esiste piu'** e T2/T3 esistono | qualcuno alza la soglia del vecchio test da 1,0 a 1,25 e chiude li' |
⚠️ **T1b e' il test piu' importante dell'elenco**, ed e' la ragione per cui questo paragrafo esiste:
il prototipo (sez. 3) mostra che **a k=1 l'implementazione simmetrica e quella asimmetrica danno lo
stesso identico numero**. Un test di simmetria scritto sul caso di default passa sempre e non
controlla niente — la stessa firma dei test di `book_live` scoperti senza potenza a libro flat.
---
## 6. IL GATE MANCANTE — `GATE SCALA-01`
### 6.1 Perche' serve un gate NUOVO
Vedi §0: nessun gate esistente vede la scala. Il gate deve quindi vivere sull'unico asse su cui la
scala si vede: **crescita e rovina** (`g(k)`, maxDD, distanza dalla liquidazione), non Sharpe.
### 6.2 Le condizioni — tutte necessarie
Per portare `book_scale_k` da `k0` a `k1`:
| | condizione | misurata come |
|---|---|---|
| **A1** | `k1` e' il **gradino successivo** di `SCALA_LADDER` (1,00 → 1,25). Nessun valore intermedio. | confronto con la costante di codice |
| **A2** | `k0` e' in produzione da **≥180 giorni** con il criterio passato **ogni giorno** | giornale `data/live/scale_history.jsonl` (append-only) + registrazioni del sorvegliante |
| **A3** | **C1–C5 di WORST-DAY** ricalcolati **oggi** a `k1`, tutti PASSA — non ereditati dal 22/08 | ri-esecuzione di `r0822c_worst_day` sui dati correnti |
| **A4** | **C7 = G2**: `n_asset · frac · k1 · disaster_sl_pct ≤ 0,50` | §4.5 |
| **A5** | **C8 = G1**: `n_asset · frac · k1 ≤ LEVA_LORDA_MAX` | §4.1 |
| **A6** | **ANTI-RECENCY**: il criterio deve passare **anche escludendo gli ultimi 90 giorni**: `k_ammesso(r[:-90]) ≥ k1` | §6.3 |
| **A7** | `r0726_fee_sensitivity` **rifatto** al nuovo lordo e la conclusione «liquidation fee irrilevante» **ri-derivata o ritirata** | §3.4 |
| **A8** | la modifica e' **una riga di config + una riga di giornale**, zero codice. Se serve toccare `LEVA_LORDA_MAX`, **non e' una decisione di config** | diff |
| **A9** | i test T1–T11 (§5) esistono e passano **prima** | pytest |
### 6.3 A6 — l'anti-recency, e come si misura davvero
La forma verificabile e': **il gradino dev'essere autorizzato anche dai dati che non contengono la
finestra recente che ha motivato la richiesta.** E' l'analogo, sull'asse della scala, di
`select_cell_insample`.
Il prototipo (sez. 7b) misura la meccanica su `k_ammesso = ½·k*(drift a −2 SE, coda troncata al
bound strutturale)`:
| finestra | n | drift punto | drift −2SE | k ammesso | ≥1,25x? |
|---|---|---|---|---|---|
| tutta la storia | 2.719 | 17,11% | 8,78% | **3,52** | SI |
| esclusi ultimi 90 g | 2.629 | 16,70% | 8,15% | **3,20** | SI |
| esclusi ultimi 365 g | 2.354 | 19,52% | 10,20% | 3,80 | SI |
| solo ultimi 365 g | 365 | 1,59% | −13,69% | 0,03 | NO |
e la sensibilita' a un tratto recente **artificialmente buono** (ultimi 90 giorni riscritti a un
drift dato, sull'intera storia):
| ultimi 90 g portati a | drift −2SE | k ammesso | delta |
|---|---|---|---|
| 0%/anno | 7,82% | 3,12 | −0,40 |
| +50%/anno | 9,47% | 3,80 | +0,28 |
| **+100%/anno** | 11,11% | **4,45** | **+0,93** |
| +200%/anno | 14,35% | 5,63 | +2,10 |
**Il −2 SE fa gran parte del lavoro** (un tratto eccezionale alza insieme il drift *e* la sua
incertezza), **ma non basta**: il criterio resta una funzione dei dati recenti, e la protezione vera
e' la **doppia valutazione** di A6.
⚠️ **Tre onesta' obbligatorie su questa sezione:**
1. `k_ammesso` del prototipo e' **deliberatamente semplificato** (troncatura al bound, nessuna GPD):
serve a mostrare la **meccanica**, non a produrre il numero. Il gate reale usa la versione di
`r0822c_worst_day`.
2. **Oggi A6 non morde**: `k_ammesso ≈ 3,5x` contro un gradino di 1,25x. Il vincolo che morde e'
quello del §4.5 (disaster-SL). **A6 serve al gradino successivo, non a questo** — e va scritto
ora, quando non e' comodo per nessuno, non quando servira'.
3. La prima stesura di questa sezione presentava «solo ultimi 365 giorni» come l'esempio di *«un anno
buono»*: **falso**, quell'anno e' stato debole (drift 1,59%). Quella riga mostra solo che un
campione corto ha un SE enorme. La riga che risponde alla domanda vera e' la tabella dello shock.
### 6.4 La cadenza — `scale_watch`
Sorvegliante giornaliero (proposto: `src/live/scale_watch.py` + `scripts/live/scale_watch.py` in
`cron_daily.sh`; **non implementato**), che risponde a **tre domande diverse** — e sono davvero tre,
perche' hanno **tre azioni diverse** e nessuna e' deducibile dalle altre:
| domanda | esito | azione |
|---|---|---|
| la config dichiara una scala che il **giornale** non ha mai autorizzato? | 🚨 | ricostruire come ci e' arrivata; scendere a `k0` |
| il criterio che autorizzo' la scala **corrente** passa ancora oggi? | 🚨 | **scendere di un gradino** |
| il **prodotto** `frac·scala·n_asset` ha superato `LEVA_LORDA_MAX`? | 🚨 | bloccare l'esecuzione |
Piu' due requisiti che il progetto ha gia' imparato a pagare:
- **tre stati, non due**: `OK` / `ALLARME` / **`NON MISURABILE`** (serie troppo corta, dati mancanti).
*«Non vedo» non e' «va tutto bene»* — `paper_dvolspread`, `venue_watch`.
- **disciplina degli allarmi**: una allerta per streak (`lock_step`), non una per giro; e il
messaggio deve dire **cosa** (decide) **e perche'** (ripara), con il valore **letto**, mai cablato.
E deve **riportare la frequenza del ramo di fallback** (§4.3), che oggi e' 0/1.442 e che se salisse
sopra il **2% in 90 giorni** invalida la regola «scala solo sul percorso fidato».
⚠️ **Cio' che il sorvegliante NON puo' fare: impedire la modifica.** Chi ha accesso al file puo'
scriverci dentro. Il sorvegliante rende la modifica **visibile entro 24 ore** e **attribuibile**.
E' la stessa onesta' di `venue_watch`: non protegge il saldo, compra tempo.
---
## 7. IL COSTO DI NON FARLO, E IL RISCHIO DI FARLO
### 7.1 Il costo di restare a 1,00x
**~€165/mese di versamenti equivalenti, a costo zero, per sempre** (§0): 14,7a → **12,9a** al
capitale-rendita a €500/m netto fisco. Sulla scala delle leve misurate dal progetto sta **sotto**
«versare o no» (da *mai* a 16 anni) e «quando versare» (fattore 6), e **sopra** tutto cio' che la
ricerca ha prodotto in due mesi: l'ottimizzazione del peso SKH01 vale **+0,030 di Sharpe** e ha
fallito il suo gate; il gradino vale **1,8 anni**.
Ed e' **l'unica leva grande che non richiede soldi nuovi**.
### 7.2 Il rischio di farlo, in una tabella
| grandezza | k=1,00 | **k=1,25** | fonte |
|---|---|---|---|
| peggior giorno **possibile** (chiusura) | 14,32% | **17,90%** | replicato, §4.5 |
| peggior giorno **vissuto** (chiusura) | 3,95% | 4,94% | replicato |
| peggior **minimo** intra-giorno | 7,33% | 9,16% | WORST-DAY (lente accoppiata) |
| **costo di un disaster-SL** | 30,0% | **37,5%** | §4.5 ← **la guardia che morde** |
| margine di liquidazione | 15,8x | 10,1x | §4.5 |
| maxDD storico | 12,17% | 15,01% | §0 |
| costo funding non modellato | ~1,53%/a | ~1,91%/a | FUNDING §35 |
| costo di una liquidazione (fee 1%) | 1,00% eq | 1,25% eq | §3.4, da ricalcolare |
### 7.3 La riga che WORST-DAY ha dichiarato, e che va tenuta in cima alla decisione
> Se si credono **entrambe** le riserve insieme — drift a −2 SE **e** un giorno a −10% ogni anno —
> il drift atteso e' **−0,61%/anno**, e **a quel punto non e' il gradino a essere sbagliato, e' il
> libro**: chi usa quella combinazione sta chiedendo di spegnere anche il conto di oggi.
Questa specifica non risolve quel dilemma e non deve: lo rende **esplicito**. Chi rifiuta il gradino
in nome di entrambe le riserve deve dire cosa propone di fare del libro a k=1.
### 7.4 Cosa questa specifica **non** rende sicuro
- **La lente e' close-only** dove non usa i numeri intra-giorno di WORST-DAY. Su una regola a **un
giorno** close-only e' **esattamente cieca** (25/07, riconfermato dallo scettico il 22/08). Sul
canale **prop/funded** — dove il vincolo binding e' una barriera di DD giornaliera — **questa
specifica non si applica** e la lente accoppiata resta obbligatoria.
- **`g(k)` assume ritorno indipendente dalla size.** A $635 e' innocuo (SLIP-AUDIT: nessun impatto
misurabile, e un fill ha preso il 21,9% del volume della sua barra 5m in un range di 1,08 bps), ma
**quella misura ha una data di scadenza** — la partecipazione scala lineare col capitale.
- **Il funding non e' in nessuna serie di questo documento.** Il libro paga gia' ~1,5%/anno che
nessun backtest sottrae; il gradino lo amplifica del 25%. E' dentro C5, ma il **livello a k=1**
resta un difetto aperto del progetto, non di questa specifica.
- **La leva non compra probabilita', compra fretta** (PROP-ALLOC). Il gradino accorcia la mediana;
non alza `P(arrivare)` in modo proporzionale, e **alza la varianza del percorso**.
---
## 8. PROCEDURA DI ATTIVAZIONE (checklist, in ordine)
1. Scrivere `tests/test_book_scale.py` con T1–T11 (§5). **Prima.** T7 deve passare **contro il
codice attuale** (la chiave non esiste ancora → default 1,0 → bit-exact).
2. Implementare §2.2 (`scala` in `book_net_target`, `_scala()`, `LEVA_LORDA_MAX`, `SCALA_LADDER`,
`scala` nel report e nel print di `book_execute`). **Senza toccare `config/live.json`**: la chiave
assente vale 1,0, il libro non cambia, i test lo dimostrano.
3. Cancellare `test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` **con la nota** (§3.4).
4. Scrivere `scale_watch` (§6.4) + `data/live/scale_history.jsonl` con la riga iniziale
`{"scala": 1.00, "dal": "...", "gate": "iniziale, nessun gradino"}`, e aggiungerlo a
`cron_daily.sh` **e** al backup di `/opt/docker/scripts/backup.sh` (il giornale non e' ricostruibile).
5. **Lasciare girare a 1,00 per almeno 30 giorni** con il sorvegliante attivo. Una manopola nuova si
guarda funzionare **prima** di girarla.
6. Solo allora: eseguire `GATE SCALA-01` (§6.2). Se PASSA, una riga di config + una di giornale.
7. Rifare `r0726_fee_sensitivity` al nuovo lordo (A7) e ripubblicare la conclusione.
**Il punto 5 non e' burocrazia: e' l'unico modo di scoprire che il sorvegliante e' rotto mentre la
leva e' ancora 1,00.**
---
## 9. NODI APERTI — dichiarati, non nascosti
1. **Il disaster-SL rotolante** (§4.5, ⚠️): letto nel codice, non misurato. Il log **non permette**
di separare i ri-piazzamenti dalle prime aperture. Se davvero rotola, `B` e' il costo di *un*
episodio e un ribasso lento ne produce piu' d'uno. **E' il nodo piu' serio di questo documento**,
perche' tocca la guardia che morde per prima. Follow-up: aggiungere `pos_prima` al log di
`ensure_disaster_sl` e ri-misurare — **non ricostruibile a posteriori**.
2. **`LEVA_LORDA_MAX = 1.25` e' una scelta di processo, non una misura.** Dall'invariante del §4.5 il
massimo sostenibile sarebbe 1,67x. Il valore basso e' il cricchetto. Chi vuole 1,50x deve toccare
codice — **ed e' voluto**.
3. **La banda d'ancora non e' stata girata su nessun numero di questo documento.** Tutte le serie
sono all'ancora canonica con la correzione ×0,89 sul drift. Le grandezze usate per decidere
(bound strutturale, lordo, invarianti) sono **aritmetiche** e non ancorate; `g(k)` e `k_ammesso`
**lo sono** — e vanno lette con la banda del libro (Sharpe FULL de-luckato 1,95, banda [1,81; 2,12]).
4. **Il gradino non e' stato autorizzato qui.** Questo documento specifica come si autorizza.
---
## 10. IL PROTOTIPO
`scripts/research/r0822e_scale_proto.py` — **0,9 s**, nessuna rete, nessuna scrittura.
```
nice -n 19 timeout 900 uv run python scripts/research/r0822e_scale_proto.py
```
Sezioni: (0) isolamento verificato · (1) sanity bit-exact vs produzione · (2) la catena letta dal
sorgente · (3) simmetria + controllo positivo · (4) l'ordine col clamp · (5) replica di WORST-DAY ·
(6) guardie · (6-bis) stati dell'equity · (7) il gate mancante.
**Replica a k=1,00 (sez. 5) — il motivo per cui le righe scalate di questo documento non sono
numeri copiati:**
| grandezza | prototipo | pubblicato | scarto |
|---|---|---|---|
| drift de-luckato ×0,89 | 17,11% | 17,11% | 0,00% |
| vol annua | 11,37% | 11,37% | −0,00% |
| Sharpe de-luckato | 1,505 | 1,510 | −0,005 |
| peggior giorno (chiusura) | −3,95% | −3,94% | −0,01% |
| data del peggior giorno | **2020-03-13** | 2020-03-13 | coincide |
| morso del clamp | **3 / 5.648** | 3 / 5.650 | 2 righe di bordo* |
| bound strutturale | **−14,32%** | −14,32% | 0,00% |
\* differenza di costruzione: il prototipo scarta la prima barra per asset (serve `vol_prev`).
---
*Documento scritto senza modificare nulla in produzione. Il branch `research/wave-0822` e' quello da
cui il cron esegue: ogni riga di `src/`, `config/`, `scripts/live/`, `scripts/cron_*.sh` e `tests/`
e' rimasta identica.*
+4
View File
@@ -20,5 +20,9 @@ mkdir -p logs
# = 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
# 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.
uv run python scripts/live/trades_db.py --sync --quiet || true
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_book.log 2>&1
+8
View File
@@ -39,5 +39,13 @@ mkdir -p logs
# decidono su queste serie: un monitor fermo produce silenzio, e il silenzio sembra uno zero.
# Va DOPO tutti i monitor, altrimenti misura lo stato di ieri.
uv run python scripts/live/monitor_health.py --quiet || true
# Giornale di bordo. Gira alle 00:30 UTC -> chiude IERI (giorno completo) e apre OGGI.
# Sola lettura: feed certificato, cron_book.log, trades.db. Il campo `nota` non lo tocca.
uv run python scripts/live/journal.py --giorno "$(date -u -d yesterday +%F)" --quiet || true
uv run python scripts/live/journal.py --quiet || true
# Analista di bordo: un modello scrive la prosa del giorno CHIUSO nel campo `analisi`.
# Scrive SOLO li': non tocca `nota` (operatore) ne' le sezioni misurate, e se non risponde
# la pagina resta senza analisi invece di mostrare quella di ieri. Costo: 1 chiamata/giorno.
uv run python scripts/live/analista.py --giorno "$(date -u -d yesterday +%F)" --quiet || true
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1
+99
View File
@@ -0,0 +1,99 @@
"""L'analista di bordo: fa scrivere a un modello l'analisi del giorno nel campo `analisi`.
uv run python scripts/live/analista.py # oggi
uv run python scripts/live/analista.py --giorno 2026-08-21
uv run python scripts/live/analista.py --modello claude-opus-5
uv run python scripts/live/analista.py --secco # stampa e non salva
NON tocca `nota` (dell'operatore) e non tocca nessuna sezione misurata: scrive SOLO in `analisi`.
Se il modello non risponde, l'analisi di ieri NON diventa quella di oggi: si registra l'errore
e la sezione resta vuota. Sola lettura su tutto il resto; nessun ordine.
"""
from __future__ import annotations
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live import analista as A # noqa: E402
from src.live import journal as J # noqa: E402
from src.live import tradesdb as T # noqa: E402
GIORNI_STORICO = 10
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
def storico(con, g: date, n: int = GIORNI_STORICO) -> str:
"""Le righe essenziali dei giorni precedenti: serve per vedere COSA E' CAMBIATO."""
righe = ["giorno equity delta fill rt posizioni"]
for i in range(n, 0, -1):
d = (g - timedelta(days=i)).isoformat()
r = con.execute("SELECT pnl, libro FROM journal WHERE giorno=?", (d,)).fetchone()
if not r:
continue
import json as _j
p, lb = _j.loads(r["pnl"] or "{}"), _j.loads(r["libro"] or "{}")
pos = " ".join(f"{a} ${v['posizione']:+,.0f}" for a, v in sorted((lb.get("asset") or {}).items()))
dq = p.get("delta_equity")
righe.append(f"{d} {p.get('equity_fine') or float('nan'):8,.2f} "
f"{' n/d' if dq is None else f'{dq:+8.2f}'} "
f"{p.get('fill', 0):>4} {p.get('roundtrip_chiusi', 0):>2} {pos or 'flat'}")
return "\n".join(righe)
def scrivi(con, g: date, modello: str, secco: bool = False, verbose: bool = True) -> str:
voce = J.costruisci(con, g)
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)
# La guardia si applica a TUTTO cio' che il modello ha ricevuto, non alla sola pagina:
# i numeri del blocco storico gli sono stati dati, citarli non e' inventare. (Primo giro:
# validando sulla sola pagina il tripwire bocciava un'equity vera presa dallo storico —
# una guardia piu' stretta del contratto produce allarmi che si imparera' a ignorare.)
fonte = A.senza_analisi(pagina) + "\n" + st
testo, errore = A.interroga(prompt, modello=modello)
if errore:
stato, motivi = "errore", [errore]
testo = ""
else:
stato, motivi = A.valida(testo, fonte)
if stato == "rifiutata":
testo = ""
if verbose:
print(f" {g} modello {modello} -> {stato}"
+ (f" ({'; '.join(motivi)})" if motivi else ""))
if secco:
print("\n" + (testo or "(niente da salvare)"))
return stato
if testo:
con.execute("""UPDATE journal SET analisi=?, analisi_modello=?, analisi_ts=?,
analisi_stato=?, analisi_motivi=? WHERE giorno=?""",
(testo, modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat()))
else:
# niente analisi: si REGISTRA il perche' e NON si lascia in pagina quella di ieri
con.execute("""UPDATE journal SET analisi='', analisi_modello=?, analisi_ts=?,
analisi_stato=?, analisi_motivi=? WHERE giorno=?""",
(modello, T.ora(), stato, "; ".join(motivi) or None, g.isoformat()))
con.commit()
J.scrivi_file(con, voce)
return stato
if __name__ == "__main__":
con = T.connect()
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
st = scrivi(con, g, modello=_arg("--modello", A.MODELLO_DEFAULT),
secco="--secco" in sys.argv[1:], verbose="--quiet" not in sys.argv[1:])
con.close()
sys.exit(0 if st in ("ok", "sospetta") else 1)
+8 -1
View File
@@ -23,6 +23,8 @@ import json
import sys
from pathlib import Path
from datetime import datetime, timezone
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
@@ -210,7 +212,12 @@ def _run():
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})")
log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
# `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"]))
+67
View File
@@ -0,0 +1,67 @@
"""Giornale di bordo: scrive la voce del giorno nel DB e in `docs/journal/YYYY-MM-DD.md`.
uv run python scripts/live/journal.py # voce di oggi
uv run python scripts/live/journal.py --giorno 2026-08-21
uv run python scripts/live/journal.py --backfill 60 # ricostruisce gli ultimi N giorni
uv run python scripts/live/journal.py --nota "testo" # annota il giorno (operatore)
SOLA LETTURA su feed, log e venue: non tocca il libro, non manda ordini.
Il campo `nota` e' l'UNICO posto per il testo libero, e non viene mai riscritto da un
ricalcolo automatico.
"""
from __future__ import annotations
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live import journal as J # noqa: E402
from src.live import tradesdb as T # noqa: E402
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
def una(con, g: date, nota: str | None = None, verbose: bool = True) -> Path:
voce = J.costruisci(con, g)
J.salva(con, voce, nota=nota)
f = J.scrivi_file(con, voce)
if verbose:
p = voce["pnl"]
d = p.get("delta_equity")
print(f" {g} equity {p.get('equity_fine') or float('nan'):,.2f} "
f"({'n/d' if d is None else f'{d:+.2f}'}) "
f"fill {p['fill']} · rt {p['roundtrip_chiusi']} · giri {voce['salute']['giri_book']}/24"
f" -> {f.relative_to(ROOT)}")
return f
if __name__ == "__main__":
# 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"))
from trades_db import sync as _sync
_sync(verbose=False)
con = T.connect()
nota = _arg("--nota")
if (n := _arg("--backfill")):
oggi = datetime.now(timezone.utc).date()
for i in range(int(n), -1, -1):
g = oggi - timedelta(days=i)
v = J.costruisci(con, g)
# un giorno senza giri di book non e' una voce: e' assenza di dato
if v["salute"]["giri_book"] == 0:
continue
J.salva(con, v); J.scrivi_file(con, v)
tot = con.execute("SELECT COUNT(*) c FROM journal").fetchone()["c"]
print(f" voci nel giornale: {tot}")
else:
g = date.fromisoformat(_arg("--giorno", datetime.now(timezone.utc).date().isoformat()))
una(con, g, nota=nota, verbose="--quiet" not in sys.argv[1:])
con.close()
+105
View File
@@ -0,0 +1,105 @@
"""Sincronizza `data/live/trades.db` — il libro di bordo dei trade, allineato col tempo.
uv run python scripts/live/trades_db.py --sync # idempotente, da cron
uv run python scripts/live/trades_db.py --report # stato + P&L a video
uv run python scripts/live/trades_db.py --reconcile # incrocio delle tre fonti
SOLA LETTURA sul venue e sui log: non manda ordini, non tocca il libro.
Il DB sta in `data/live/`, che il backup rotativo della VPS gia' copre — a differenza di
`logs/cron_book.log`, che e' oggi l'unica sede dell'ora vera dei fill.
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live import tradesdb as T # noqa: E402
def sync(verbose: bool = True) -> dict:
con = T.connect()
runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace")) if T.CRON_LOG.exists() else []
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])
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"]
if verbose:
print(f" fills: +{n_f} (totale {tot}) | equity: +{n_e} (totale {tot_e}) | round-trip {n_r}")
con.close()
return dict(nuovi_fill=n_f, nuovi_equity=n_e, roundtrip=n_r, totale_fill=tot)
def reconcile() -> None:
runs = T.parse_cron_log(T.CRON_LOG.read_text(errors="replace"))
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" concordano : {len(rec['ok'])}")
print(f" solo nel log : {len(rec['solo_log'])}")
print(f" solo nel jsonl : {len(rec['solo_jsonl'])}")
print(f" prezzi divergenti : {len(rec['prezzi_divergenti'])} (oltre l'arrotondamento di stampa)")
for f in rec["solo_log"]:
print(f" solo-log {f.ts_utc} {f.asset} {f.side} {f.qty} @ {f.price}")
for r in rec["solo_jsonl"]:
print(f" solo-jsonl {r['ts_utc'][:10]} {r['asset']} {r['side']} {r['filled']} @ {r['price']}")
# il venue: autorevole ma TRONCATO -> si riporta cosa vede, non lo si usa per riparare
try:
from src.live.deribit import DeribitRead
d = DeribitRead()
for ins in ("BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"):
t = d.trade_history(ins, limit=100)
print(f" venue {ins:<20}: {len(t)} trade visibili (l'endpoint TRONCA: non e' un backfill)")
except Exception as e:
print(f" venue: NON LETTO ({type(e).__name__}) — non e' 'zero trade', e' 'non misurato'")
def report() -> None:
con = T.connect()
n = con.execute("SELECT COUNT(*) c FROM fills").fetchone()["c"]
if not n:
print(" DB vuoto: lanciare --sync"); return
p = con.execute("SELECT MIN(ts_utc) a, MAX(ts_utc) b FROM fills").fetchone()
print(f"\n fill registrati : {n} dal {p['a'][:16]} al {p['b'][:16]}")
eq = con.execute("SELECT ts_utc, equity FROM equity ORDER BY ts_utc").fetchall()
if eq:
e0, e1 = eq[0]["equity"], eq[-1]["equity"]
picco = max(r["equity"] for r in eq)
print(f" equity : ${e0:,.2f} -> ${e1:,.2f} ({e1 - e0:+.2f}, {100*(e1/e0-1):+.2f}%)"
f" | picco ${picco:,.2f} | {len(eq)} letture")
rt = con.execute("SELECT * FROM roundtrips ORDER BY ts_out").fetchall()
lordo = sum(r["pnl_lordo"] for r in rt)
fee_rt = sum(r["fee_quota"] for r in rt)
fee_tot = con.execute("SELECT COALESCE(SUM(fee),0) s FROM fills").fetchone()["s"]
vinc = sum(1 for r in rt if r["pnl_netto"] > 0)
print(f" round-trip chiusi : {len(rt)} ({vinc} in utile) "
f"lordo {lordo:+.2f} | fee allocate {fee_rt:.2f} | netto {lordo - fee_rt:+.2f}")
print(f" fee totali pagate : {fee_tot:.4f}")
ap = T.stato_aperto(con)
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]})")
else:
print(" posizioni aperte : nessuna (flat)")
print(f" ultimo sync : {T.get_meta(con, 'ultimo_sync', 'mai')}")
con.close()
if __name__ == "__main__":
args = sys.argv[1:]
if "--reconcile" in args:
reconcile()
elif "--report" in args:
report()
else:
sync(verbose="--quiet" not in args)
+728
View File
@@ -0,0 +1,728 @@
#!/usr/bin/env python
"""r0822_adaptive_horizon.py — ADAPTIVE-HORIZON TSMOM: stimare *quanto indietro guardare*
dai dati (change-point / persistenza) invece di fissarlo a 30/90/180 giorni.
DOMANDA
-------
Un TSMOM a lookback ADATTIVO batte il TSMOM a orizzonti FISSI 30/90/180 (TP01), in modo
MARGINALE e ONESTO?
COSA NON E' (i due vicini gia' bocciati, per non rifarli)
---------------------------------------------------------
* 02/07 "velocita' trend regime-condizionata": pesava i TRE orizzonti fissi in funzione del
percentile di vol -> risulto' un tilt-30d statico travestito (pctl 0.71 vs il null dei pesi
statici casuali). Li' gli orizzonti restavano 30/90/180: cambiavano solo i PESI.
* 29/06 "meta-allocazione dinamica" fra sleeve: peggiore dei pesi fissi. Li' l'oggetto adattato
erano i pesi di PORTAFOGLIO, non un parametro della strategia.
Qui l'oggetto adattato e' **l'orizzonte stesso**: L_t stimato da un rilevatore di change-point
(CUSUM) o di persistenza (variance-ratio, Hurst). Mai provato nel progetto.
FORMA FUNZIONALE (scelta perche' rende il confronto strutturalmente pulito)
--------------------------------------------------------------------------
TP01 canonico e' il blend di sign(c[i]/c[i-h]-1) su h = (30, 90, 180) giorni, long-flat,
vol-target 20%, leva cap 2x. Ma (30, 90, 180) == (L/3, L, 2L) con **L = 90 giorni**.
Quindi il candidato e' LA STESSA FUNZIONE con L che varia nel tempo:
blend='triple' -> horizons_t = (L_t/3, L_t, 2*L_t) (TP01 = L_t costante a 90g)
blend='single' -> horizons_t = (L_t,) (un solo orizzonte adattivo)
Tutto il resto (long-flat, target_vol 20%, vol_win 30g, leva 2x, fee 0.05%/lato, cadenza,
50/50 BTC+ETH) e' IDENTICO al baseline: **l'unico grado di liberta' e' l'orizzonte**.
=> il null non e' "meglio di zero", e' "meglio del TSMOM fisso a PARI GEOMETRIA".
FAMIGLIA DICHIARATA **PRIMA** DI GUARDARE (conteggio al RIALZO)
--------------------------------------------------------------
rilevatore (3): cusum | vratio | hurst
parametro (3): cusum k in {2,4,8} sigma-cumulate; vratio/hurst finestra W in {60,120,250}g
mappatura (2): direct (piu' persistenza / piu' tempo dall'ultimo change-point -> L piu' LUNGO)
inverse (specchiata) [il verso NON e' ovvio a priori => e' un asse, non una scelta]
blend (2): single | triple
timeframe (2): 1d | 12h [>= 12h: sotto, costi+overfit dominano (19/06)]
------------------------------------------------------------------
= 3 x 3 x 2 x 2 = 36 celle x 2 TF = **72 valutazioni**.
Costanti FISSATE A PRIORI e non cercate (dichiarate perche' sono gradi di liberta' non contati
nella griglia): Lmin=20g, Lmax=200g (contengono 30..180 del canonico); q=5g del variance-ratio;
scale del Hurst k in {1,2,4,8}g; finestra della percentile causale M=250g. Per questo il
deflated-Sharpe e' riportato ANCHE a N=144 e N=216 trial (sensibilita' del verdetto al conteggio,
regola del 30/07): un verdetto che si ribalta col conteggio si cita come tale.
GATE (tutti dell'harness del progetto, nessuno riscritto)
---------------------------------------------------------
A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe (NO selezione su hold-out)
A.marginal_vs_tp01 -> ADDS/HEDGE/NOISE/REDUNDANT/DILUTES + corr al baseline
A.causality_ok -> ricalcolo su prefisso (nessun look-ahead nella costruzione del segnale)
A.anchor_luck_band -> stima onesta = MEDIANA delle 24 ancore, non l'ancora 00:00
A.anchor_luck_delta -> confronto adattivo-vs-fisso: MEDIANA delle DIFFERENZE APPAIATE
A.implausible_sharpe -> Sharpe troppo bello = rischio fuori dal campione
A.eval_weights_smallcap-> haircut reale a $600 (min-order $5)
null del de-levering -> esiste k<1 che da' al FISSO lo stesso maxDD con Sharpe >=?
differenza APPAIATA PER ANNO -> un vantaggio concentrato in un anno e' un evento, non un meccanismo
Causalita': ogni rolling ha min_periods e finestra all'indietro; la CUSUM e' un loop in avanti
con reset (prefix-stable); la percentile e' rolling causale; nessuna statistica full-sample.
Runtime atteso ~3-6 min su 2 core. Nessun file toccato fuori da questo.
"""
from __future__ import annotations
import sys
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT))
import altlib as A # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
ASSETS = A.CERTIFIED
HOLDOUT = A.HOLDOUT
# --- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) --------------
LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte adattivo, in giorni
FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> il baseline
TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30
VR_Q_DAYS = 5 # passo del variance-ratio
HURST_K_DAYS = (1, 2, 4, 8) # scale del Hurst
PCTL_M_DAYS = 250 # finestra della percentile causale
TFS = ("1d", "12h")
DETECTORS = ("cusum", "vratio", "hurst")
PARAMS = {"cusum": (2.0, 4.0, 8.0), "vratio": (60, 120, 250), "hurst": (60, 120, 250)}
MAPPINGS = ("direct", "inverse")
BLENDS = ("single", "triple")
# ===========================================================================
# RILEVATORI -> lookback adattivo L_t (in barre). Tutti causali.
# ===========================================================================
def _logret(c: np.ndarray) -> np.ndarray:
r = np.zeros(len(c))
r[1:] = np.log(c[1:] / c[:-1])
return r
def _roll_pctl(x: np.ndarray, m: int) -> np.ndarray:
"""Percentile CAUSALE del valore corrente nella sua finestra all'indietro (rolling rank).
Warmup -> 0.5 (neutro): non si inventa un regime dove non c'e' campione."""
s = pd.Series(x)
u = s.rolling(m, min_periods=max(20, m // 4)).rank(pct=True).values
return np.where(np.isfinite(u), u, 0.5)
def _cusum_age(c: np.ndarray, k: float, bpd: int) -> np.ndarray:
"""Eta' (in barre) dall'ultimo change-point CUSUM a due code su rendimenti standardizzati.
Loop in avanti con reset: dipende solo dal passato -> stabile su prefisso."""
lr = _logret(c)
sig = pd.Series(lr).rolling(30 * bpd, min_periods=max(5, 15 * bpd)).std().values
s = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0)
s = np.nan_to_num(s)
n = len(c)
age = np.zeros(n)
sp = sm = 0.0
last = 0
for i in range(n):
sp = max(0.0, sp + s[i])
sm = min(0.0, sm + s[i])
if max(sp, -sm) > k:
sp = sm = 0.0
last = i
age[i] = i - last
return age
def _vratio(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray:
"""Variance ratio VR = Var(r_q) / (q * Var(r_1)) su finestra w_days. >1 = persistente."""
lr = _logret(c)
qb = max(2, VR_Q_DAYS * bpd)
lg = np.log(c)
lrq = np.zeros(len(c))
lrq[qb:] = lg[qb:] - lg[:-qb]
lrq[:qb] = np.nan
wb = max(20, w_days * bpd)
v1 = pd.Series(lr).rolling(wb, min_periods=wb // 2).var().values
vq = pd.Series(lrq).rolling(wb, min_periods=wb // 2).var().values
with np.errstate(invalid="ignore", divide="ignore"):
vr = vq / (qb * v1)
return vr
def _hurst(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray:
"""Hurst da scaling della volatilita': sd(r_k) ~ k^H. Regressione di log sd su log k
(x fisso -> forma chiusa). Tutto rolling causale."""
lg = np.log(c)
wb = max(20, w_days * bpd)
xs, ys = [], []
for kd in HURST_K_DAYS:
kb = max(1, kd * bpd)
d = np.full(len(c), np.nan)
d[kb:] = lg[kb:] - lg[:-kb]
sd = pd.Series(d).rolling(wb, min_periods=wb // 2).std().values
with np.errstate(invalid="ignore", divide="ignore"):
ys.append(np.log(np.where(sd > 0, sd, np.nan)))
xs.append(np.log(kb))
X = np.asarray(xs, float)
Y = np.vstack(ys) # (n_scale, n_bar)
xm = X.mean()
denom = float(((X - xm) ** 2).sum())
with warnings.catch_warnings(): # warmup = colonne tutte-NaN
warnings.simplefilter("ignore", RuntimeWarning)
ym = np.nanmean(Y, axis=0)
H = ((X - xm)[:, None] * (Y - ym[None, :])).sum(axis=0) / denom
return H
_STAT_CACHE: dict = {}
def _stat_key(df: pd.DataFrame, det: str, par) -> tuple:
ts = df["timestamp"].values
return (det, par, len(df), int(ts[0]), int(ts[-1]))
def lookback_bars(df: pd.DataFrame, det: str, par, mapping: str) -> np.ndarray:
"""L_t in BARRE, dentro [Lmin, Lmax] giorni. Cache chiusa sul CONTENUTO del frame
(lunghezza + primo/ultimo timestamp) -> un prefisso NON prende la cache del pieno,
altrimenti causality_ok verrebbe ingannata dalla cache stessa."""
bpd = A.bars_per_day(df)
lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd)
c = df["close"].values.astype(float)
key = _stat_key(df, det, par)
if key not in _STAT_CACHE:
if det == "cusum":
_STAT_CACHE[key] = _cusum_age(c, float(par), bpd)
elif det == "vratio":
_STAT_CACHE[key] = _roll_pctl(_vratio(c, int(par), bpd), PCTL_M_DAYS * bpd)
elif det == "hurst":
_STAT_CACHE[key] = _roll_pctl(_hurst(c, int(par), bpd), PCTL_M_DAYS * bpd)
else:
raise ValueError(det)
stat = _STAT_CACHE[key]
if det == "cusum":
base = np.clip(stat, lo, hi)
L = base if mapping == "direct" else (lo + hi - base)
else:
u = np.clip(stat, 0.0, 1.0)
L = lo + u * (hi - lo) if mapping == "direct" else hi - u * (hi - lo)
return np.clip(np.round(L), 2, hi).astype(int)
# ===========================================================================
# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target
# ===========================================================================
def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
n = len(c)
i = np.arange(n)
j = i - h
ok = j >= 0
s = np.zeros(n)
jj = np.where(ok, j, 0)
s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0)
return s, ok
def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray:
hs = [L] if blend == "single" else [
np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)]
acc = np.zeros(len(c))
cnt = np.zeros(len(c))
for h in hs:
s, ok = _sign_at(c, np.asarray(h, int))
acc[ok] += s[ok]
cnt[ok] += 1
out = np.zeros(len(c))
nz = cnt > 0
out[nz] = acc[nz] / cnt[nz]
return out
def fixed_target(df: pd.DataFrame) -> np.ndarray:
"""BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico."""
c = df["close"].values.astype(float)
bpd = A.bars_per_day(df)
acc = np.zeros(len(c))
cnt = np.zeros(len(c))
for hd in FIXED_H:
s, ok = _sign_at(c, np.full(len(c), hd * bpd, int))
acc[ok] += s[ok]
cnt[ok] += 1
d = np.zeros(len(c))
nz = cnt > 0
d[nz] = acc[nz] / cnt[nz]
return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV)
def const_factory(tf: str, L: int, blend: str):
"""TSMOM a orizzonte COSTANTE L giorni, STESSA geometria del candidato adattivo.
blend='triple' -> (L/3, L, 2L): con L=90 e' ESATTAMENTE TP01 canonico 30/90/180."""
def fn(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
bpd = A.bars_per_day(df)
Lb = np.full(len(c), max(2, int(round(L * bpd))), int)
d = np.clip(_dir_from_L(c, Lb, blend), 0, None)
return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV)
fn.__name__ = f"const_L{L}_{blend}"
return fn
def factory(tf: str, det: str, par, mapping: str, blend: str):
"""target_fn(df) -> posizione per barra, decisa con dati <= close[i]."""
def fn(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
L = lookback_bars(df, det, par, mapping)
d = np.clip(_dir_from_L(c, L, blend), 0, None)
return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV)
fn.__name__ = f"adapt_{det}{par}_{mapping}_{blend}"
return fn
GRID = [dict(det=d, par=p, mapping=m, blend=b)
for d in DETECTORS for p in PARAMS[d] for m in MAPPINGS for b in BLENDS]
# CONTROLLO dichiarato DOPO aver visto che il vincitore ha L_t incollato al bordo (sezione 3):
# il null "TSMOM fisso 30/90/180" NON basta piu', perche' confonde due cose diverse —
# "adattare l'orizzonte paga" e "un orizzonte piu' CORTO paga su questo campione".
# Il controllo giusto e' il migliore fra gli orizzonti COSTANTI della stessa banda, scelto
# in-sample-only. Trial aggiuntivi, contati al rialzo.
CONST_L = (20, 30, 45, 60, 90, 120, 150, 200)
CONST_GRID = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS]
# ESTENSIONE dichiarata DOPO aver visto che l'ottimo costante cade sul BORDO BASSO della banda
# (L=20g = LMIN_D): un ottimo al bordo non e' un ottimo, e' il punto in cui si e' smesso di
# guardare. Si estende SOTTO e si guarda se e' un plateau o una scivolata verso il rumore.
# +4 valori x 1 blend x 2 TF = 8 trial in piu' (contati al rialzo).
CONST_L_EXT = (5, 7, 10, 15)
# ===========================================================================
# UTILITA' DI MISURA
# ===========================================================================
def _dd(s: pd.Series) -> float:
eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float))
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def _cagr(s: pd.Series) -> float:
s = s.dropna()
if len(s) < 5:
return float("nan")
eq = float(np.prod(1.0 + s.values))
yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25)
return eq ** (1 / max(yrs, 1e-6)) - 1.0
def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame:
"""Stessa convenzione di trend_portfolio.resample_tf, con ancora spostata di `off` ore
(r0702_tp01_offset.py). NB: si usa `offset=`, non `origin=` (pandas ignora origin su 7D)."""
g = df_1h.copy()
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
idx.name = "dt"
g.index = idx
out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
out = out.dropna(subset=["open"])
out["datetime"] = out.index
epoch = pd.Timestamp("1970-01-01", tz="UTC")
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return out.reset_index(drop=True)[
["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
def daily_from_target(fn, tf: str) -> pd.Series:
return A.candidate_daily(fn, tf=tf)
def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]:
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
rows = []
for y, g in J.groupby(J.index.year):
if len(g) < 40:
continue
rows.append(dict(year=int(y), n=len(g),
sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2),
d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2),
ret_cand=round(float(np.prod(1 + g["C"].values) - 1) * 100, 1),
ret_base=round(float(np.prod(1 + g["B"].values) - 1) * 100, 1)))
return rows
def delever_null(cand: pd.Series, base: pd.Series) -> dict:
"""Il fisso, DE-LEVERATO a pari maxDD del candidato, che Sharpe fa?
Scalare le POSIZIONI per k scala gross e fee insieme -> il netto scala per k e lo Sharpe
e' INVARIANTE: percio' un candidato che compra solo meno DD non e' un miglioramento."""
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
dd_c, dd_b = _dd(J["C"]), _dd(J["B"])
if dd_c >= dd_b or dd_b <= 0:
return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
nota="il candidato NON riduce il maxDD: il null non si applica")
lo, hi = 0.0, 1.0
for _ in range(60):
k = 0.5 * (lo + hi)
if _dd(k * J["B"]) < dd_c:
lo = k
else:
hi = k
k = 0.5 * (lo + hi)
return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
dd_base_delev=round(_dd(k * J["B"]), 4),
sharpe_cand=round(A._sh(J["C"]), 3),
sharpe_base_delev=round(A._sh(k * J["B"]), 3),
superato=bool(A._sh(J["C"]) > A._sh(k * J["B"])))
_ANCHOR_CACHE: dict = {}
def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series:
"""Serie giornaliera 50/50 BTC+ETH della strategia `fn` ribilanciata sull'ancora `off`
(resample del 1h certificato). Cache per (name, hours, off)."""
key = (name, hours, off)
if key not in _ANCHOR_CACHE:
ser = {}
for a in ASSETS:
dfo = resample_offset(A.get(a, "1h"), hours, off)
ev = A.eval_weights(dfo, A._call_target(fn, dfo, a))
ser[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
_ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
return _ANCHOR_CACHE[key]
def adaptivity(tf: str, det: str, par, mapping: str) -> dict:
"""Quanto si MUOVE davvero L_t (diagnostica di MECCANISMO, non di rendimento).
Una cella con L_t incollato a un bordo NON e' adattiva: e' un TSMOM a orizzonte
COSTANTE travestito, e non appartiene alla famiglia che si sta testando."""
fr_lo, fr_hi, iqr, med = [], [], [], []
for a in ASSETS:
df = A.get(a, tf)
bpd = A.bars_per_day(df)
L = lookback_bars(df, det, par, mapping) / bpd
fr_lo.append(float(np.mean(L <= LMIN_D + 1)))
fr_hi.append(float(np.mean(L >= LMAX_D - 1)))
iqr.append(float(np.percentile(L, 75) - np.percentile(L, 25)))
med.append(float(np.median(L)))
return dict(frac_lo=float(np.mean(fr_lo)), frac_hi=float(np.mean(fr_hi)),
iqr_days=float(np.mean(iqr)), med_days=float(np.mean(med)),
adattiva=bool(np.mean(fr_lo) < 0.5 and np.mean(fr_hi) < 0.5
and np.mean(iqr) >= 10.0))
def hr(t: str = "") -> None:
print("\n" + "=" * 78)
if t:
print(t)
print("=" * 78)
# ===========================================================================
def main() -> None:
print(__doc__.split("Runtime atteso")[0])
# -- 0. SANITY: il mio baseline DEVE essere TP01 canonico, bit-per-bit ----------------
hr("0. SANITY — il baseline 'fisso' e' TP01 canonico (se non lo e', ogni Δ e' inventato)")
tp = TrendPortfolio(**CANONICAL)
worst = 0.0
for a in ASSETS:
df = A.get(a, "1d")
worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df)))))
base_1d = daily_from_target(fixed_target, "1d")
tp01 = A.tp01_baseline_daily()
JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna()
print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}")
print(f" max|serie_giornaliera_mia - al.tp01_baseline_daily()| = "
f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})")
assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui"
# -- 1. GRIGLIA ONESTA: cella scelta IN-SAMPLE + deflated-Sharpe ----------------------
hr(f"1. study_family_honest — {len(GRID)} celle x {len(TFS)} TF = {len(GRID)*len(TFS)} trial")
rep = A.study_family_honest("ADAPT-H", factory, GRID, TFS)
ch = rep["chosen"]
print(f" celle valutate : {rep['n_cells']}")
print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']} "
f"Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}")
print(f" deflated-Sharpe (N={rep['n_cells']}) : {rep['deflated_sharpe']} "
f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}")
print(f" earns_slot_marginal : {rep['earns_slot_marginal']}")
print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}")
print("\n top-8 IN-SAMPLE (come si sceglie onestamente):")
for r in rep["rows"][:8]:
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} "
f"{r['tf']:>3} {r['params']}")
# sensibilita' del verdetto al CONTEGGIO dei trial (regola 30/07)
fn_ch = factory(tf=ch["tf"], **ch["params"])
d_ch = daily_from_target(fn_ch, ch["tf"])
allsr = [r["full_sharpe"] for r in rep["rows"]]
print("\n sensibilita' del DSR al numero di trial dichiarati:")
for n_decl in (len(GRID) * len(TFS), 144, 216):
pad = allsr + [float(np.mean(allsr))] * max(0, n_decl - len(allsr))
dsr, _ = A.deflated_sharpe(A._sh(d_ch), pad, d_ch)
print(f" N={n_decl:>4} DSR={dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}")
# cosa avrebbe scelto un disonesto (diagnostica, NON una decisione)
diag = []
for r in rep["rows"][:12]:
s_all = daily_from_target(factory(tf=r["tf"], **r["params"]), r["tf"])
diag.append({**r, "hold": round(A._sh(s_all[s_all.index >= HOLDOUT]), 3)})
diag.sort(key=lambda r: -r["hold"])
print("\n (diagnostica) le stesse 12 celle riordinate per HOLD-OUT — cio' che NON si fa:")
for r in diag[:4]:
print(f" HOLD {r['hold']:+.3f} IS {r['insample_sharpe']:+.3f} {r['tf']:>3} {r['params']}")
# -- 2. NUMERI del candidato vs il FISSO a pari geometria -----------------------------
hr("2. CANDIDATO vs TSMOM FISSO 30/90/180 — stessa geometria, stesso TF, stesse fee")
base_ch = daily_from_target(fixed_target, ch["tf"])
J = pd.concat({"C": d_ch, "B": base_ch}, axis=1, join="inner").dropna()
for lab, sl in (("FULL", J), ("HOLD-OUT", J[J.index >= HOLDOUT])):
if len(sl) < 30:
continue
print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% "
f"CAGR {_cagr(sl['C'])*100:6.2f}% | fisso Sh {A._sh(sl['B']):+.3f} "
f"DD {_dd(sl['B'])*100:5.2f}% CAGR {_cagr(sl['B'])*100:6.2f}% "
f"| dSh {A._sh(sl['C'])-A._sh(sl['B']):+.3f}")
print(f" corr(adattivo, fisso) = {J['C'].corr(J['B']):.3f}")
print("\n DIFFERENZA APPAIATA PER ANNO (un vantaggio in un anno solo e' un evento):")
rows = paired_by_year(d_ch, base_ch)
for r in rows:
print(f" {r['year']} adatt Sh {r['sh_cand']:+.2f} ({r['ret_cand']:+7.1f}%) "
f"fisso Sh {r['sh_base']:+.2f} ({r['ret_base']:+7.1f}%) dSh {r['d_sh']:+.2f}")
ds = [r["d_sh"] for r in rows]
print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} "
f"mediana dSh {np.median(ds):+.2f} media {np.mean(ds):+.2f}")
# -- 3. LOOKBACK: si muove davvero? ---------------------------------------------------
hr("3. IL LOOKBACK ADATTIVO SI MUOVE? (se sta al bordo e' un TSMOM COSTANTE travestito)")
for a in ASSETS:
df = A.get(a, ch["tf"])
bpd = A.bars_per_day(df)
L = lookback_bars(df, ch["params"]["det"], ch["params"]["par"],
ch["params"]["mapping"]) / bpd
lo, hi = LMIN_D, LMAX_D
print(f" {a}: L_t giorni min {L.min():.0f} p25 {np.percentile(L,25):.0f} "
f"mediana {np.median(L):.0f} p75 {np.percentile(L,75):.0f} max {L.max():.0f} "
f"| al bordo basso {np.mean(L<=lo+1)*100:.1f}% al bordo alto {np.mean(L>=hi-1)*100:.1f}%"
f" | |dL|/giorno mediano {np.median(np.abs(np.diff(L))):.1f}g")
# -- 4. GATE ---------------------------------------------------------------------------
hr("4. GATE")
print(A.fmt_marginal(rep["marginal"]))
cz = A.causality_ok(fn_ch, tf=ch["tf"])
print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} "
f"(prefissi controllati: {cz['checked']})")
imp = A.implausible_sharpe(d_ch)
print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} "
f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} "
f"attive {imp.get('active_frac')}")
for rr in imp["reasons"]:
print(f" - {rr}")
dl = delever_null(d_ch, base_ch)
print(f" null del de-levering : {dl}")
# small-cap a $600
print(" haircut a $600 (min-order $5):")
for a in ASSETS:
df = A.get(a, ch["tf"])
sc_c = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0)
sc_b = A.eval_weights_smallcap(df, fixed_target(df), capital=600.0)
print(f" {a}: adattivo modellato {sc_c['modeled']['sharpe']:+.3f} -> reale "
f"{sc_c['realistic']['sharpe']:+.3f} (haircut {sc_c['sharpe_haircut']:+.3f}, "
f"{sc_c['n_executed_trades']} trade) | fisso {sc_b['modeled']['sharpe']:+.3f} -> "
f"{sc_b['realistic']['sharpe']:+.3f} (haircut {sc_b['sharpe_haircut']:+.3f}, "
f"{sc_b['n_executed_trades']} trade)")
# -- 5. BANDA D'ANCORA -----------------------------------------------------------------
hours = 24 if ch["tf"] == "1d" else 12
offs = list(range(hours))
hr(f"5. BANDA D'ANCORA — {len(offs)} ancore, stima onesta = MEDIANA (mai l'ancora 00:00)")
def _series_at(off: int, which: str) -> pd.Series:
return series_at_anchor(fn_ch if which == "cand" else fixed_target, which, hours, off)
# sanity: l'ancora 0 riproduce la serie canonica
s0 = _series_at(0, "fixed")
K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna()
print(f" sanity ancora 0 vs serie canonica del fisso: max|Δ| = "
f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})")
for lab, mtr in (("Sharpe FULL", A._sh),
("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))):
b_c = A.anchor_luck_band(lambda o: _series_at(o, "cand"), offs, metric=mtr)
b_f = A.anchor_luck_band(lambda o: _series_at(o, "fixed"), offs, metric=mtr)
print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} (pctl {b_c['canonical_pctl']:.2f}) "
f"MEDIANA {b_c['median']:+.3f} banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]")
print(f" {'':<12} fisso : canonica {b_f['canonical']:+.3f} (pctl {b_f['canonical_pctl']:.2f}) "
f"MEDIANA {b_f['median']:+.3f} banda [{b_f['lo']:+.3f},{b_f['hi']:+.3f}]")
dlt = A.anchor_luck_delta(lambda o: _series_at(o, "cand"),
lambda o: _series_at(o, "fixed"), offs, metric=mtr)
print(f" {'':<12} DELTA APPAIATO: mediana {dlt['median_paired']:+.3f} "
f"positivo in {dlt['n_positive']}/{dlt['n_anchors']} ancore "
f"banda [{dlt['lo']:+.3f},{dlt['hi']:+.3f}] gate_pass={dlt['gate_pass']}")
# -- 6. ADATTIVITA' EFFETTIVA DI TUTTA LA FAMIGLIA ------------------------------------
hr("6. QUANTE DELLE 72 CELLE SONO DAVVERO ADATTIVE? (filtro di MECCANISMO, non di resa)")
print(" criterio: L_t sta a un bordo <50% del tempo E l'IQR di L_t e' >= 10 giorni")
seen, adatt_rows = {}, []
for r in rep["rows"]:
k = (r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["mapping"])
if k not in seen:
seen[k] = adaptivity(*k)
r["_adapt"] = seen[k]
if seen[k]["adattiva"]:
adatt_rows.append(r)
n_ad_cells = sum(1 for r in rep["rows"] if r["_adapt"]["adattiva"])
print(f" celle DAVVERO adattive: {n_ad_cells}/{len(rep['rows'])} "
f"(le altre sono TSMOM a orizzonte COSTANTE travestito)")
for k, v in sorted(seen.items()):
print(f" {k[0]:>3} {k[1]:>6} {str(k[2]):>5} {k[3]:<7} bordo-basso {v['frac_lo']*100:5.1f}% "
f"bordo-alto {v['frac_hi']*100:5.1f}% IQR {v['iqr_days']:5.1f}g "
f"mediana {v['med_days']:5.1f}g -> {'ADATTIVA' if v['adattiva'] else 'costante'}")
# -- 7. IL CONTROLLO CHE MANCAVA: il MIGLIOR ORIZZONTE COSTANTE ------------------------
hr(f"7. CONTROLLO L COSTANTE — {len(CONST_GRID)} celle x {len(TFS)} TF = "
f"{len(CONST_GRID)*len(TFS)} trial IN PIU'")
print(" la domanda diventa: il guadagno viene dall'ADATTARE, o dal fatto che su questo")
print(" campione un orizzonte piu' CORTO batte 30/90/180 anche se lo si tiene fisso?")
csel = A.select_cell_insample(lambda tf, L, blend: const_factory(tf, L, blend),
CONST_GRID, TFS)
cch = csel["chosen"]
print(f"\n miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} "
f"Sharpe IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}")
print(" griglia costante, in-sample (L=90 triple == TP01 canonico):")
for r in csel["rows"]:
tag = " <== TP01 canonico" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple"
and r["tf"] == "1d") else ""
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}")
fn_const = const_factory(**cch["params"], tf=cch["tf"])
d_const = daily_from_target(fn_const, cch["tf"])
JC = pd.concat({"C": d_ch, "K": d_const}, axis=1, join="inner").dropna()
print(f"\n ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti in-sample):")
for lab, sl in (("FULL", JC), ("HOLD-OUT", JC[JC.index >= HOLDOUT])):
if len(sl) < 30:
continue
print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% | "
f"costante Sh {A._sh(sl['K']):+.3f} DD {_dd(sl['K'])*100:5.2f}% | "
f"dSh {A._sh(sl['C'])-A._sh(sl['K']):+.3f}")
print(f" corr(adattivo, miglior costante) = {JC['C'].corr(JC['K']):.3f}")
print("\n differenza APPAIATA per anno vs il miglior COSTANTE:")
rows2 = paired_by_year(d_ch, d_const)
for r in rows2:
print(f" {r['year']} adatt {r['sh_cand']:+.2f} costante {r['sh_base']:+.2f} "
f"dSh {r['d_sh']:+.2f}")
d2 = [r["d_sh"] for r in rows2]
print(f" -> anni con dSh>0: {sum(1 for x in d2 if x > 0)}/{len(d2)} "
f"mediana {np.median(d2):+.2f}")
dl2 = delever_null(d_ch, d_const)
print(f" null del de-levering vs costante: {dl2}")
# ancora appaiata adattivo vs miglior costante (se stesso TF, altrimenti dichiarato)
if cch["tf"] == ch["tf"]:
for lab, mtr in (("Sharpe FULL", A._sh),
("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))):
dd2 = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o),
lambda o: series_at_anchor(fn_const, "const", hours, o),
offs, metric=mtr)
print(f" ancora {lab}: DELTA APPAIATO mediana {dd2['median_paired']:+.3f} "
f"positivo in {dd2['n_positive']}/{dd2['n_anchors']} "
f"banda [{dd2['lo']:+.3f},{dd2['hi']:+.3f}] gate_pass={dd2['gate_pass']}")
else:
print(f" (ancora appaiata non girata: TF diversi, adattivo {ch['tf']} vs "
f"costante {cch['tf']} -> le ancore non sono coppie)")
# fee sweep: l'adattivo tradà di piu', il conto va fatto
print("\n FEE SWEEP (il candidato ha piu' turnover: e' li' che muore o no)")
for nm, f_ in (("adattivo", fn_ch), ("costante", fn_const), ("TP01 30/90/180", fixed_target)):
tfx = ch["tf"] if nm != "costante" else cch["tf"]
line = []
for fee in (0.0, 0.0005, 0.001, 0.0015):
dser = A.candidate_daily(f_, tf=tfx, fee_side=fee)
line.append(f"{2*fee*100:.2f}%RT {A._sh(dser):+.3f}")
tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a))
["turnover_per_year"] for a in ASSETS])
print(f" {nm:<16} " + " ".join(line) + f" | turnover {tt:.0f}x/anno")
# -- 8. LA FAMIGLIA RISTRETTA ALLE CELLE DAVVERO ADATTIVE -----------------------------
hr("8. GATE ONESTO SULLA SOLA FAMIGLIA ADATTIVA (la domanda della missione, isolata)")
if not adatt_rows:
print(" NESSUNA cella della famiglia e' davvero adattiva -> la domanda non e'")
print(" misurabile su questa famiglia: ogni 'vincitore' e' un orizzonte costante.")
else:
best_ad = max(adatt_rows, key=lambda r: r["insample_sharpe"])
print(f" celle adattive: {len(adatt_rows)} miglior IN-SAMPLE: tf={best_ad['tf']} "
f"{best_ad['params']} IS {best_ad['insample_sharpe']} FULL {best_ad['full_sharpe']}")
fn_ad = factory(tf=best_ad["tf"], **best_ad["params"])
d_ad = daily_from_target(fn_ad, best_ad["tf"])
sm_ad = A.study_marginal("ADAPT-H(solo adattive)", fn_ad, tf=best_ad["tf"])
dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad),
[r["full_sharpe"] for r in adatt_rows], d_ad)
print(f" marginal={sm_ad['marginal_verdict']} earns_slot={sm_ad['earns_slot']} "
f"corr->TP01 {sm_ad['marginal'].get('corr_full')}")
print(f" deflated-Sharpe (N={len(adatt_rows)}) = {dsr_ad:.3f} "
f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:.3f})")
JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna()
for lab, sl in (("FULL", JA), ("HOLD-OUT", JA[JA.index >= HOLDOUT])):
if len(sl) < 30:
continue
print(f" {lab:<9} adattiva-vera Sh {A._sh(sl['C']):+.3f} | miglior costante "
f"{A._sh(sl['K']):+.3f} | TP01 {A._sh(sl['B']):+.3f}")
rows3 = paired_by_year(d_ad, d_const)
d3 = [r["d_sh"] for r in rows3]
print(f" vs miglior costante, per anno: dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)}"
f" mediana {np.median(d3):+.2f}")
# -- 9. L'OTTIMO COSTANTE E' AL BORDO: si estende la banda ----------------------------
hr("9. L'OTTIMO COSTANTE CADE SUL BORDO — plateau o scivolata verso il rumore?")
print(" un argmax al bordo della griglia non e' un ottimo: e' il punto in cui si e' smesso")
print(" di guardare. Si estende SOTTO i 20 giorni (8 trial in piu', contati al rialzo).")
ext = sorted(set(CONST_L) | set(CONST_L_EXT))
tab: dict = {}
for tfx in TFS:
print(f"\n --- TF {tfx}, blend triple (L/3, L, 2L) ---")
print(f" {'L(g)':>5} {'orizzonti':>16} {'IS':>7} {'FULL':>7} {'HOLD':>7} "
f"{'maxDD':>7} {'CAGR':>7} {'turn/a':>7} {'Sh@0.30%RT':>11}")
for L in ext:
fnx = const_factory(tfx, L, "triple")
dx = daily_from_target(fnx, tfx)
dx_h = dx[dx.index >= HOLDOUT]
dx_i = dx[dx.index < HOLDOUT]
d030 = A.candidate_daily(fnx, tf=tfx, fee_side=0.0015)
tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(fnx, A.get(a, tfx), a))
["turnover_per_year"] for a in ASSETS])
tag = " <== TP01" if L == 90 else ""
tab[(tfx, L)] = dict(IS=A._sh(dx_i), FULL=A._sh(dx), HOLD=A._sh(dx_h))
print(f" {L:>5} {f'{L//3}/{L}/{2*L}':>16} {A._sh(dx_i):>7.3f} {A._sh(dx):>7.3f} "
f"{A._sh(dx_h):>7.3f} {_dd(dx)*100:>6.2f}% {_cagr(dx)*100:>6.2f}% "
f"{tt:>7.0f} {A._sh(d030):>11.3f}{tag}")
# --- e' una CURVA con un ottimo, o un crinale di RUMORE? --------------------------
print("\n RISOLUZIONE della griglia (regola 07/08: un criterio che decide su una frazione")
print(" dello spread della griglia e' rumore anche quando 'passa'):")
for tfx in TFS:
v = [tab[(tfx, L)]["IS"] for L in ext]
jump = float(np.median(np.abs(np.diff(v))))
arg = ext[int(np.argmax(v))]
arg_orig = max(CONST_L, key=lambda L: tab[(tfx, L)]["IS"])
print(f" {tfx:>3}: argmax IS con la griglia ORIGINALE L={arg_orig}g -> con la griglia "
f"ESTESA L={arg}g ({'SPOSTATO' if arg != arg_orig else 'stabile'})")
print(f" salto IS mediano fra L ADIACENTI {jump:+.3f} contro il vantaggio del "
f"vincitore su TP01 {max(v) - tab[(tfx, 90)]['IS']:+.3f} "
f"(rapporto {jump / max(1e-9, max(v) - tab[(tfx, 90)]['IS']):.2f})")
allc = [(tf, L) for tf in TFS for L in ext]
for met in ("IS", "FULL", "HOLD"):
vals = sorted((tab[k][met] for k in allc), reverse=True)
r1d = 1 + sum(1 for x in vals if x > tab[("1d", 90)][met])
print(f" rango di TP01 (1d, L=90g) fra le {len(allc)} celle su {met}: "
f"{r1d}/{len(allc)} (suo {tab[('1d',90)][met]:+.3f}, migliore {vals[0]:+.3f})")
hr("FINE")
if __name__ == "__main__":
main()
+709
View File
@@ -0,0 +1,709 @@
"""ALT-OPT — la famiglia opzioni USDC-LINEARE di Deribit, mai guardata dal progetto (2026-08-22).
CONTESTO. Il filone VRP-QUOTE-VERE di questa stessa ondata ha trovato che `collect_chain.py`
interroga `{"currency": "BTC"|"ETH"}`, che su Deribit restituisce le sole opzioni INVERSE
(regolate in BTC/ETH). Il conto e' in USDC. Esiste accanto una famiglia USDC-LINEARE con lotti
molto piu' piccoli, e contando l'open interest sembra che su SOL/XRP/HYPE/AVAX sia molto piu'
popolata di BTC_USDC. Il progetto non l'ha mai misurata.
DOMANDA. Esiste su questa famiglia una struttura a RISCHIO DEFINITO (put credit spread, la stessa
geometria di VRP01: vendi ~-0.28, compri ~-0.10) eseguibile con qualche centinaio di dollari, con
spread e liquidita' che non se la mangino?
IPOTESI DICHIARATA PRIMA DI MISURARE: che il vincolo binding fosse la TAGLIA DEL LOTTO (e' cosi'
che il progetto ha sempre bocciato le opzioni: "BTC min 0.1 = $6.210/lotto -> FUORI a $3.000") e
che le famiglie alt, con lotti da poche decine di dollari, la superassero. Sbagliata due volte:
il lotto non e' il vincolo, e la classifica di liquidita' su cui poggia la domanda si ROVESCIA.
COSA MISURA, in ordine, fermandosi appena un vincolo la uccide:
0. l'UNITA' dei contratti — due test empirici, perche' leggerla male sposta ogni cifra in
dollari di un fattore 10-1000 (errore commesso nella prima stesura di questo file);
1. i fatti del venue (min_trade_amount, tick_size, contract_size, $ di UN lotto);
2. la liquidita' misurata in DUE modi che danno risposte opposte: open interest vs quote
effettivamente presenti. E' il risultato principale del file;
3. la struttura: delta REALIZZATI, credito eseguibile vs credito al mark, spread, fee, max-loss;
4. il collaterale e quante strutture entrano in $635;
5. quanto costerebbe raccogliere il dato (perche' il verdetto massimo qui e' LEAD).
COSA NON PUO' FARE, detto prima dei numeri: **non esiste un backtest possibile**. Il nostro
collettore raccoglie solo le inverse BTC/ETH e l'archivio ereditato pure: zero giorni di storia su
queste famiglie. Questo file misura UN ISTANTE.
VINCOLI DI PROGETTO CITATI, non aggirati:
(a) "niente short-vol da modello in deploy" (19/06). Qui i prezzi sono VERI, non modellati —
che e' un'altra cosa — ma la regola resta finche' non esiste un campione che contenga uno
STRESS. Un istante di calma non e' un campione.
(b) SOL e' escluso dall'universo DIREZIONALE per decisione dell'operatore del 22/08. Quella
decisione riguarda TP01/SKH01 congelati come terza gamba di TREND, e il suo stesso testo
dichiara che un meccanismo diverso la riapre; un put credit spread non e' direzionale in
quel senso. Ma il guardrail sul DATO vale identico: nessun file nuovo in `data/raw/`, e i
prezzi del sottostante qui vengono da `underlying_price` della catena, mai da `load_data`.
(Nota: qui SOL non viene proposto — viene BOCCIATO, e sulla misura, non sulla decisione.)
(c) il gate IV-rank>0.30 e' l'unico alpha misurato di VRP01 e su questi sottostanti NON e'
calcolabile: non abbiamo un giorno di storia di vol implicita. Senza quel gate la struttura
non e' VRP01, e' vendere vol a caso.
CONVENZIONE DI UNITA' (la trappola di questo file, verificata al §0): le opzioni INVERSE si
quotano in CRIPTO per contratto, le USDC-LINEARI in USDC per UNITA' DI SOTTOSTANTE; e
`min_trade_amount` e' in unita' di sottostante, NON in contratti — quindi non va moltiplicato per
`contract_size` (farlo gonfia XRP_USDC da $1.469 a $1.469.000 per lotto).
RETE: sole letture pubbliche, nessun ordine, pacing ~2 richieste/s, e il file SI RIFIUTA di
partire nella finestra :24-:29 (e' quando `cron_chain` usa lo stesso IP: il rate limit Deribit e'
per-IP e il progetto ha gia' avuto un guasto per questo, 29/07).
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py
nice -n 19 timeout 900 uv run python scripts/research/r0822_alt_options.py --cache
"""
from __future__ import annotations
import argparse
import json
import math
import time
from datetime import UTC, datetime
from pathlib import Path
import numpy as np
import requests
API = "https://www.deribit.com/api/v2/public"
TIMEOUT = 20
RPS = 2.0
CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/altopt_cache.json")
SHORT_DELTA, LONG_DELTA = -0.28, -0.10 # la geometria di VRP01, non ri-scelta qui
DTE_LO, DTE_HI = 4.0, 45.0 # dal settimanale di VRP01 (4-10) a ~1.5 mesi
OI_MIN = 100.0 # la soglia che usa collect_chain.py, qui sotto esame
CAPITAL = 635.0 # il conto vero
RISK_BUDGET = 0.20 # quota di conto a rischio, per la capacita'
TAKER_CAP = 0.125 # cap fee opzioni Deribit: 12.5% del premio
LINEAR = ("ETH_USDC", "BTC_USDC", "SOL_USDC", "XRP_USDC", "HYPE_USDC", "AVAX_USDC")
INVERSE = ("BTC", "ETH") # termine di paragone: il progetto le conosce
FAMS = LINEAR + INVERSE
# ---------------------------------------------------------------- rete (parca, sola lettura)
class Budget:
def __init__(self, rps: float = RPS) -> None:
self.rps, self._next, self.calls, self.err, self.r429 = rps, 0.0, 0, 0, 0
def wait(self) -> None:
now = time.monotonic()
if now < self._next:
time.sleep(self._next - now)
self._next = max(now, self._next) + 1.0 / self.rps
def guardia_finestra() -> None:
m = datetime.now(UTC).minute
if 24 <= m <= 29:
raise SystemExit(f"minuto {m}: finestra di cron_chain (:25-:29). Rilancia dopo il :30.")
def get(path: str, params: dict, b: Budget, tries: int = 3):
for k in range(tries):
b.wait()
b.calls += 1
try:
r = requests.get(f"{API}/{path}", params=params, timeout=TIMEOUT)
except Exception:
b.err += 1
time.sleep(1.5 * (k + 1))
continue
if r.status_code == 429:
b.r429 += 1
time.sleep(2.0 * (k + 1))
continue
if r.status_code != 200:
b.err += 1
time.sleep(1.0 * (k + 1))
continue
try:
return r.json()["result"]
except Exception:
b.err += 1
return None
return None
def fetch_all(b: Budget) -> dict:
"""6 chiamate: instruments + book_summary per USDC, BTC, ETH. Niente per-strumento."""
out = {}
for cur in ("USDC", "BTC", "ETH"):
out[f"inst_{cur}"] = get("get_instruments",
{"currency": cur, "kind": "option", "expired": "false"}, b) or []
out[f"summ_{cur}"] = get("get_book_summary_by_currency",
{"currency": cur, "kind": "option"}, b) or []
return out
# ---------------------------------------------------------------- Black-Scholes (r=0, come il sleeve)
def put_delta(S: float, K: float, T: float, sig: float) -> float:
if not (S > 0 and K > 0 and T > 0 and sig > 0):
return float("nan")
d1 = (math.log(S / K) + 0.5 * sig * sig * T) / (sig * math.sqrt(T))
return 0.5 * (1.0 + math.erf(d1 / math.sqrt(2.0))) - 1.0
# ---------------------------------------------------------------- unita'
def is_inv(fam: str) -> bool:
return fam in INVERSE
def usd(price: float | None, r: dict) -> float:
"""Prezzo di UN LOTTO MINIMO, in dollari.
Lineare: il prezzo e' gia' in USDC per unita' di sottostante -> x min_trade_amount.
Inverse: il prezzo e' in CRIPTO per contratto -> x min_trade_amount x sottostante.
`contract_size` NON entra: `min_trade_amount` e' gia' in unita' di sottostante (§0).
"""
if price is None:
return float("nan")
return price * r["minamt"] * (r["S"] if is_inv(r["fam"]) else 1.0)
def loss_usd(Ks: float, Kl: float, r: dict) -> float:
"""Perdita massima strutturale, per un lotto minimo.
Lineare: esatta, (Ks-Kl) x quantita'. Inverse: il payoff e' in cripto (max(0,K-S)/S), il caso
peggiore e' a S=Kl e vale (Ks-Kl)/Kl cripto = (Ks-Kl) dollari A QUEL PREZZO -> stessa formula,
ma APPROSSIMATA (il valore in dollari dipende da dove finisce S). Dichiarata, non nascosta.
"""
return (Ks - Kl) * r["minamt"]
# ---------------------------------------------------------------- margine (FONTE SECONDARIA)
def im_short_put(S: float, K: float, mark_lot: float, r: dict) -> float:
"""Initial margin di una short put, in USDC, per UN LOTTO, formula PUBBLICATA da Deribit:
IM = max(max(0.15 - OTM/S, 0.10), 0.075) x S + mark [per unita' di sottostante]
⚠️ FONTE SECONDARIA, dichiarata: l'API PUBBLICA non espone il margine. E il numero che decide
davvero — se Deribit NETTA le due gambe di uno spread in margine standard — non e' leggibile
da qui: si conferma con `private/get_margins`, che richiede una chiave (lettura, ma non
pubblica) e quindi NON e' stato usato.
"""
if not (S > 0 and K > 0):
return float("nan")
otm = max(0.0, S - K) / S # una put e' OTM quando K < S
return max(max(0.15 - otm, 0.10), 0.075) * S * r["minamt"] + max(0.0, mark_lot)
# ---------------------------------------------------------------- vista
def build(raw: dict) -> list[dict]:
inst, summ = {}, {}
for cur in ("USDC", "BTC", "ETH"):
for i in raw.get(f"inst_{cur}") or []:
inst[i["instrument_name"]] = i
for s in raw.get(f"summ_{cur}") or []:
summ[s["instrument_name"]] = s
rows, now_ms = [], datetime.now(UTC).timestamp() * 1000.0
for name, i in inst.items():
s = summ.get(name)
if s is None:
continue
fam = name.split("-")[0]
dte = (float(i.get("expiration_timestamp") or 0) - now_ms) / 86400_000.0
bid, ask = s.get("bid_price"), s.get("ask_price")
bid = float(bid) if bid else None # Deribit manda 0/None per "nessun lato"
ask = float(ask) if ask else None
iv, S = s.get("mark_iv"), s.get("underlying_price")
K, T = float(i.get("strike") or 0), max(dte, 0.0) / 365.25
typ = "P" if i.get("option_type") == "put" else "C"
rows.append(dict(
name=name, fam=fam, typ=typ, K=K, dte=dte, S=float(S) if S else float("nan"),
iv=float(iv) if iv else float("nan"),
delta=(put_delta(float(S), K, T, float(iv) / 100.0)
if (S and iv and K and T > 0 and typ == "P") else float("nan")),
bid=bid, ask=ask, mid=((bid + ask) / 2 if (bid and ask) else None),
mark=float(s.get("mark_price") or 0.0),
oi=float(s.get("open_interest") or 0.0), vol=float(s.get("volume") or 0.0),
vol_usd=float(s.get("volume_usd") or 0.0),
tick=float(i.get("tick_size") or 0.0), minamt=float(i.get("min_trade_amount") or 0.0),
csize=float(i.get("contract_size") or 0.0),
blockmin=float(i.get("block_trade_min_trade_amount") or 0.0),
taker=float(i.get("taker_commission") or 0.0003), settle=i.get("settlement_currency"),
))
return rows
def rel_spread(r: dict) -> float:
if r["bid"] is None or r["ask"] is None or not r["mid"]:
return float("nan")
return (r["ask"] - r["bid"]) / r["mid"]
# ---------------------------------------------------------------- §0 le unita'
def unita(rows: list[dict], b: Budget, rete: bool) -> None:
print("\n" + "=" * 104)
print("0. L'UNITA' DEI CONTRATTI — due test empirici, prima di qualunque cifra in dollari")
print("=" * 104)
print(" Test 1 (dai dati gia' scaricati): `volume_usd / (volume x mark_price)`.")
print(" = 1 se `volume` e' in UNITA' DI SOTTOSTANTE; = contract_size se e' in CONTRATTI;")
print(" = prezzo del sottostante se il mark e' in cripto (inverse).")
print(f"\n {'famiglia':<10} {'n oss':>6} {'vol_usd/(vol*mark)':>20} {'c_size':>8} "
f"{'sottost.':>10} {'lettura':>26}")
for f in FAMS:
rr = [r for r in rows if r["fam"] == f and r["vol"] > 0 and r["mark"] > 0]
if not rr:
continue
q = float(np.median([r["vol_usd"] / (r["vol"] * r["mark"]) for r in rr]))
cs, S = rr[0]["csize"], float(np.nanmedian([r["S"] for r in rr]))
if abs(q - 1.0) < 0.35:
lettura = "unita' di sottostante"
elif abs(q - cs) / max(cs, 1) < 0.35:
lettura = "CONTRATTI"
elif abs(q - S) / max(S, 1) < 0.35:
lettura = "cripto/contratto (inverse)"
else:
lettura = "?? non concludente"
print(f" {f:<10} {len(rr):>6} {q:>20.4f} {cs:>8g} {S:>10.2f} {lettura:>26}")
if not rete:
print("\n Test 2 (size minima nel book): non girato (--cache non usa la rete).")
return
print("\n Test 2 (rete): la size MINIMA presente nel book. Se compare esattamente")
print(" `min_trade_amount`, allora e' quello il lotto minimo IN UNITA' DI SOTTOSTANTE.")
print(f"\n {'famiglia':<10} {'min_amt':>9} {'c_size':>8} {'min size vista':>15} "
f"{'libri':>6} {'verdetto':>12}")
for f in LINEAR:
cand = sorted([r for r in rows if r["fam"] == f and (r["bid"] or r["ask"])],
key=lambda r: -r["oi"])[:4]
sizes, nb = [], 0
for r in cand:
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 10}, b)
if not ob:
continue
nb += 1
for side in ("bids", "asks"):
sizes += [float(l[1]) for l in (ob.get(side) or [])]
if not sizes:
continue
mn, ma, cs = min(sizes), cand[0]["minamt"], cand[0]["csize"]
v = "confermato" if abs(mn - ma) < 1e-9 else ("CONTRATTI" if abs(mn - ma * cs) < 1e-9
else "non concl.")
print(f" {f:<10} {ma:>9g} {cs:>8g} {mn:>15g} {nb:>6} {v:>12}")
print("\n ⚠️ Errore commesso e corretto in sessione: la prima stesura moltiplicava")
print(" min_trade_amount x contract_size, e dava XRP_USDC a $1.468.900 di nozionale per")
print(" lotto. Un numero assurdo e' un test: nessun venue retail quota un minimo da $1,5M.")
# ---------------------------------------------------------------- §1 fatti del venue
def famiglie(rows: list[dict]) -> dict:
print("\n" + "=" * 104)
print("1. I FATTI DEL VENUE — public/get_instruments + get_book_summary_by_currency")
print("=" * 104)
print(f"{'famiglia':<10} {'strum':>6} {'min_amt':>9} {'c_size':>7} {'tick':>9} "
f"{'sottost.$':>11} {'$/lotto':>9} {'taker':>7} {'settle':>7}")
tab = {}
for f in FAMS:
sub = [r for r in rows if r["fam"] == f]
if not sub:
continue
S = float(np.nanmedian([r["S"] for r in sub]))
a = sub[0]
tab[f] = dict(n=len(sub), minamt=a["minamt"], csize=a["csize"], tick=a["tick"], S=S,
lot=a["minamt"] * S, taker=a["taker"],
nliq=sum(1 for r in sub if r["oi"] >= OI_MIN))
print(f"{f:<10} {len(sub):>6} {a['minamt']:>9.4g} {a['csize']:>7.4g} {a['tick']:>9.5g} "
f"{S:>11.2f} {tab[f]['lot']:>9.2f} {a['taker']:>7.4g} {str(a['settle']):>7}")
print("\n `$/lotto` = min_trade_amount x sottostante = il NOZIONALE di un lotto (NON il premio,")
print(" che e' molto minore). E' la granularita' minima imposta dal venue.")
print(" `taker` per le opzioni Deribit e' una frazione del SOTTOSTANTE con cap al 12.5% del")
print(" premio: sull'ala molto OTM morde il CAP, non la percentuale.")
print(" 📌 Il lotto minimo NON e' il vincolo: da $242 (ETH_USDC) a $1.469 (XRP_USDC), contro")
print(" i $7.738 di BTC inverse su cui il progetto aveva costruito il muro delle opzioni.")
return tab
# ---------------------------------------------------------------- §2 liquidita', due modi
def liquidita(rows: list[dict], tab: dict) -> None:
print("\n" + "=" * 104)
print("2. LA LIQUIDITA', MISURATA IN DUE MODI CHE DANNO RISPOSTE OPPOSTE")
print("=" * 104)
print(" A) open interest >= 100 = quante POSIZIONI sono aperte (il filtro che usa")
print(" `collect_chain.py`, ereditato da bite, e la misura su cui poggia la domanda).")
print(" B) quote a DUE LATI presenti adesso = quanti strumenti sono NEGOZIABILI a un prezzo")
print(" noto. Sui soli put nella finestra 4-45 giorni, cioe' dove vive la struttura.")
print(f"\n{'famiglia':<10} {'A: OI>=100':>11} {'rango A':>8} {'put 4-45g':>10} "
f"{'B: 2 lati':>10} {'% quotati':>10} {'rango B':>8}")
stat = {}
for f in FAMS:
puts = [r for r in rows if r["fam"] == f and r["typ"] == "P" and DTE_LO <= r["dte"] <= DTE_HI]
two = [r for r in puts if r["bid"] is not None and r["ask"] is not None]
stat[f] = dict(oi=tab[f]["nliq"], puts=len(puts), two=len(two),
pct=(len(two) / len(puts) if puts else float("nan")))
rA = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["oi"]))}
rB = {f: i + 1 for i, f in enumerate(sorted(stat, key=lambda x: -stat[x]["pct"]))}
for f in sorted(FAMS, key=lambda x: -stat[x]["oi"]):
s = stat[f]
print(f"{f:<10} {s['oi']:>11} {rA[f]:>8} {s['puts']:>10} {s['two']:>10} "
f"{s['pct']:>9.0%} {rB[f]:>8}")
# la correlazione di rango si CALCOLA, non si racconta: sulle sole famiglie lineari,
# cioe' quelle su cui la domanda del filone e' stata posta.
a = np.array([rA[f] for f in LINEAR], float)
c = np.array([rB[f] for f in LINEAR], float)
rho = float(np.corrcoef(a, c)[0, 1])
print(f"\n 📌 LA CLASSIFICA SI ROVESCIA. Correlazione di rango fra i due criteri sulle sei")
print(f" famiglie lineari: {rho:+.2f}. SOL_USDC e' 1ª per open interest fra le lineari e")
print(" ULTIMA di tutte per quote presenti; BTC_USDC, dichiarato 'praticamente morto' (5")
print(" strumenti con OI>=100), ha quasi tutti i put quotati a due lati. L'open interest")
print(" misura posizioni ACCUMULATE,")
print(" non la possibilita' di negoziare ADESSO — e sull'universo lineare le due cose non")
print(" hanno quasi nulla a che vedere.")
print(" ⚠️ Ricaduta sul codice di produzione: `collect_chain.py` prefiltra a OI>=100. Sulle")
print(" inverse non fa danno (sono quotate comunque), ma NON e' un filtro di liquidita' e il")
print(" suo nome lo lascia credere. 4ª occorrenza di 'una riga presente non e' un dato")
print(" presente' — qui in versione: 'una posizione aperta non e' un prezzo negoziabile'.")
print("\n" + "-" * 104)
print("2b. E ALLORA DOV'E' LA LIQUIDITA' CHE SI VEDE NEL VOLUME? Nel mercato a BLOCCHI,")
print(" che ha un minimo d'ordine suo — e non e' raggiungibile con $635.")
print("-" * 104)
print(f"{'famiglia':<10} {'con volume 24h':>15} {'volume 24h $':>14} {'blocco min':>11} "
f"{'blocco min $':>13} {'x il conto':>11}")
for f in FAMS:
sub = [r for r in rows if r["fam"] == f]
if not sub:
continue
nv = sum(1 for r in sub if r["vol"] > 0)
vusd = sum(r["vol_usd"] for r in sub)
bm = sub[0]["blockmin"]
bm_usd = bm * tab[f]["S"] * (1.0 if not is_inv(f) else 1.0)
print(f"{f:<10} {nv:>15} {vusd:>14,.0f} {bm:>11g} {bm_usd:>13,.0f} "
f"{bm_usd/CAPITAL:>10.0f}x")
print("\n 📌 SOL_USDC ha molti piu' strumenti con volume nelle ultime 24h che quotati a due")
print(" lati: quelle opzioni si negoziano, ma via blocchi/RFQ. Il minimo di blocco e' un")
print(" fatto del venue, non un'opinione, e a $635 e' fuori portata di due ordini di")
print(" grandezza. **Non si conclude che il mercato alt non esista: si conclude che la parte")
print(" a cui questo conto ha accesso — il book continuo — e' quella sottile.**")
# ---------------------------------------------------------------- §3 la struttura
def scegli(rows: list[dict], fam: str, eseguibile: bool) -> list[dict]:
"""Per ogni scadenza: la put piu' vicina a -0.28 (corta) e a -0.10 (lunga).
`eseguibile=False` -> fra TUTTI gli strike (la struttura che si vorrebbe).
`eseguibile=True` -> solo fra gli strike che hanno il LATO che serve: per APRIRE bisogna
vendere la corta al BID e comprare l'ala all'ASK. E' una restrizione
asimmetrica, ed e' quella giusta: un book a un lato solo non e' un
book vuoto, e' un book che serve solo una direzione.
"""
puts = [r for r in rows if r["fam"] == fam and r["typ"] == "P"
and DTE_LO <= r["dte"] <= DTE_HI and np.isfinite(r["delta"])]
out = []
for e in sorted({round(r["dte"], 4) for r in puts}):
g = [r for r in puts if round(r["dte"], 4) == e]
gs = [r for r in g if r["bid"] is not None] if eseguibile else g
gl = [r for r in g if r["ask"] is not None] if eseguibile else g
if not gs or not gl:
continue
s = min(gs, key=lambda r: abs(r["delta"] - SHORT_DELTA))
lo = min(gl, key=lambda r: abs(r["delta"] - LONG_DELTA))
if s["K"] > lo["K"]:
out.append({"dte": e, "short": s, "long": lo, "n": len(g)})
return out
def fee_lotto(r: dict) -> float:
"""Fee taker di UNA gamba, un lotto: min(0.03% del nozionale, 12.5% del premio)."""
return min(r["taker"] * r["S"] * r["minamt"], TAKER_CAP * usd(r["mark"], r))
def struttura(rows: list[dict]) -> dict:
print("\n" + "=" * 104)
print(f"3. LA STRUTTURA — put credit spread {SHORT_DELTA}/{LONG_DELTA}, finestra "
f"{DTE_LO:.0f}-{DTE_HI:.0f} giorni, per UN LOTTO MINIMO")
print("=" * 104)
print(" `f_venue` = (bid_corta - ask_ala) / (mark_corta - mark_ala) = quanta parte del")
print(" credito che il venue stesso considera equo resta dopo aver attraversato lo spread.")
print(" E' il gemello del `f` misurato dal progetto il 30/07 (0,73 = errore del MODELLO):")
print(" qui il modello non c'entra, il riferimento e' il mark del venue.")
print(f"\n{'famiglia':<10} {'scad':>4} {'d corta':>8} {'d ala':>7} {'spr% corta':>11} "
f"{'spr% ala':>9} {'cred mark$':>11} {'cred eseg$':>11} {'f_venue':>8} "
f"{'fee/cred':>9} {'maxloss$':>9}")
res = {}
for f in FAMS:
gg = scegli(rows, f, eseguibile=True)
if not gg:
print(f"{f:<10} {'0':>4} nessuna scadenza con corta-bid E ala-ask presenti")
continue
acc = {k: [] for k in ("ds", "dl", "ss", "sl", "cm", "ce", "fee", "ml")}
for g in gg:
s, lo = g["short"], g["long"]
cm = usd(s["mark"], s) - usd(lo["mark"], lo)
ce = usd(s["bid"], s) - usd(lo["ask"], lo)
if cm <= 0:
continue
acc["ds"].append(s["delta"]); acc["dl"].append(lo["delta"])
acc["ss"].append(rel_spread(s)); acc["sl"].append(rel_spread(lo))
acc["cm"].append(cm); acc["ce"].append(ce)
acc["fee"].append(2.0 * (fee_lotto(s) + fee_lotto(lo)) / cm)
acc["ml"].append(loss_usd(s["K"], lo["K"], s) - ce)
if not acc["cm"]:
print(f"{f:<10} {len(gg):>4} nessuna struttura con credito positivo al mark")
continue
m = {k: float(np.nanmedian(v)) if v else float("nan") for k, v in acc.items()}
m["fv"] = float(np.median([e / c for e, c in zip(acc["ce"], acc["cm"])]))
m["n"] = len(acc["cm"])
res[f] = m
print(f"{f:<10} {m['n']:>4} {m['ds']:>8.3f} {m['dl']:>7.3f} {m['ss']*100:>10.1f}% "
f"{m['sl']*100:>8.1f}% {m['cm']:>11.2f} {m['ce']:>11.2f} {m['fv']:>8.3f} "
f"{m['fee']*100:>8.1f}% {m['ml']:>9.2f}")
print("\n ⚠️ `spr%` puo' essere NaN quando la gamba ha un lato solo: e' negoziabile per")
print(" aprire (il lato che serve c'e') ma non ha un mid, quindi non ha uno spread definito.")
print(" `fee/cred` = 2 gambe x (apertura + chiusura) col cap 12.5%. VRP01 tiene FINO A")
print(" SCADENZA (nessuna gestione infra-settimana, il profit-take e' REFUTED il 30/07),")
print(" quindi il costo realistico e' ~la META' di questa colonna + la delivery fee.")
print(f"\n --- controllo: gli STESSI strike scelti fra TUTTI gli strike (non solo quotati) ---")
print(f" {'famiglia':<10} {'ideale d corta':>15} {'eseg. d corta':>14} {'ideale d ala':>13} "
f"{'eseg. d ala':>12} {'scad ideali':>12} {'scad eseg.':>11}")
for f in FAMS:
gi, ge = scegli(rows, f, False), scegli(rows, f, True)
if not gi:
continue
di = float(np.median([g["short"]["delta"] for g in gi]))
li = float(np.median([g["long"]["delta"] for g in gi]))
de = float(np.median([g["short"]["delta"] for g in ge])) if ge else float("nan")
le = float(np.median([g["long"]["delta"] for g in ge])) if ge else float("nan")
print(f" {f:<10} {di:>15.3f} {de:>14.3f} {li:>13.3f} {le:>12.3f} "
f"{len(gi):>12} {len(ge):>11}")
print(" Se la colonna `eseg.` si allontana dal target -0.28/-0.10, la struttura che si")
print(" comprerebbe NON e' quella richiesta: la griglia degli strike quotati la deforma.")
print("\n --- CONTROLLO POSITIVO: questo snapshot e' di SABATO. E' atipico? ---")
print(" Non posso aspettare una settimana, ma posso puntare lo stesso strumento su una")
print(" grandezza che il progetto ha GIA' misurato su 19 settimane di giorni feriali:")
print(" attraversare lo spread sul canonico BTC/ETH inverse costa ~10% del credito (30/07).")
for f in INVERSE:
if f in res:
print(f" {f:<4} inverse: costo di attraversamento oggi = "
f"{(1-res[f]['fv'])*100:5.1f}% del credito (atteso ~10%)")
print(" Se coincide, il sabato non sta degradando i book che il progetto conosce, e la")
print(" lettura sulle famiglie nuove non e' un artefatto del giorno. **E' un controllo")
print(" debole** (una grandezza sola, un istante), non una prova di tipicita'.")
robustezza_dte(rows)
return res
def robustezza_dte(rows: list[dict]) -> None:
"""L'UNICO grado di liberta' di questo file, esposto invece che nascosto.
VRP01 e' settimanale (tenor 7g, finestra 4-10). Qui la finestra e' stata allargata a 4-45
per dare piu' scadenze alle famiglie alt, che ne hanno poche. Allargare una finestra e'
una scelta, e una scelta va misurata: se la conclusione dipendesse da essa, sarebbe la
conclusione a essere stata scelta.
"""
global DTE_HI
print("\n --- ROBUSTEZZA: la finestra DTE e' il mio unico grado di liberta' ---")
print(f" {'famiglia':<10} {'finestra':>9} {'scad':>5} {'f_venue':>8} {'spr% corta':>11} "
f"{'cred eseg$':>11} {'maxloss$':>9}")
orig = DTE_HI
try:
for hi in (10.0, 45.0):
DTE_HI = hi
for f in FAMS:
gg = scegli(rows, f, eseguibile=True)
fv, ss, ce, ml = [], [], [], []
for g in gg:
s, lo = g["short"], g["long"]
cm = usd(s["mark"], s) - usd(lo["mark"], lo)
c = usd(s["bid"], s) - usd(lo["ask"], lo)
if cm <= 0:
continue
fv.append(c / cm); ss.append(rel_spread(s)); ce.append(c)
ml.append(loss_usd(s["K"], lo["K"], s) - c)
w = f"{DTE_LO:.0f}-{hi:.0f}g"
if not fv:
print(f" {f:<10} {w:>9} {len(gg):>5} {'n/d':>8}")
continue
print(f" {f:<10} {w:>9} {len(fv):>5} {np.median(fv):>8.3f} "
f"{np.nanmedian(ss)*100:>10.1f}% {np.median(ce):>11.2f} "
f"{np.median(ml):>9.2f}")
print()
finally:
DTE_HI = orig
print(" 📌 Da leggere sull'ORDINE, non sulla terza cifra: le quote si muovono fra un giro e")
print(" l'altro (su tre snapshot a pochi minuti ETH_USDC ha dato 0.947-0.983, le inverse")
print(" 0.847-0.942, le alt 0.79-0.87, SOL sempre negativo). Cio' che NON si muove e' la")
print(" graduatoria, e alla finestra VERA di VRP01 (4-10g) e' piu' netta, non meno.")
def profondita(rows: list[dict], fams: list[str], b: Budget) -> None:
print("\n" + "=" * 104)
print("3b. PROFONDITA' IN CIMA AL BOOK (public/get_order_book depth=5) sulle gambe scelte")
print("=" * 104)
print(f"{'famiglia':<10} {'gamba':<6} {'n':>3} {'lotti al best':>14} {'$ premio al best':>17} "
f"{'prezzo/tick':>12} {'max|d venue-BS|':>17}")
for f in fams:
gg = scegli(rows, f, eseguibile=True)[:4] # max 4 scadenze: budget di rete
for lato, key, side in (("corta", "short", "bids"), ("ala", "long", "asks")):
lots, prem, tk, dd = [], [], [], []
for g in gg:
r = g[key]
ob = get("get_order_book", {"instrument_name": r["name"], "depth": 5}, b)
if not ob or not (ob.get(side) or []):
continue
px, sz = float(ob[side][0][0]), float(ob[side][0][1])
lots.append(sz / r["minamt"] if r["minamt"] else float("nan"))
prem.append(usd(px, r) * (sz / max(r["minamt"], 1e-12)))
if r["tick"]:
tk.append(px / r["tick"])
gv = (ob.get("greeks") or {}).get("delta")
if gv is not None and np.isfinite(r["delta"]):
dd.append(abs(float(gv) - r["delta"]))
if not lots:
print(f"{f:<10} {lato:<6} {'0':>3} book vuoto sul lato che serve")
continue
print(f"{f:<10} {lato:<6} {len(lots):>3} {np.median(lots):>14.1f} "
f"{np.median(prem):>17.2f} {(np.median(tk) if tk else float('nan')):>12.1f} "
f"{(max(dd) if dd else float('nan')):>17.4f}")
print("\n `lotti al best` = size in cima al book / lotto minimo: quante strutture minime")
print(" entrano senza muovere il prezzo. `prezzo/tick` e' il controllo d'artefatto del")
print(" 30/07: un prezzo a 1-2 tick e' una griglia, non un prezzo.")
print(" `max|d venue-BS|` valida il MIO delta contro le greche del venue: se non e' piccolo,")
print(" tutta la selezione degli strike qui sopra e' sbagliata.")
# ---------------------------------------------------------------- §4 collaterale e capacita'
def collaterale(rows: list[dict], res: dict) -> None:
print("\n" + "=" * 104)
print(f"4. COLLATERALE E CAPACITA' — conto reale ${CAPITAL:.0f}")
print("=" * 104)
print(f"{'famiglia':<10} {'IM corta+ala$':>14} {'max-loss$':>10} {'n @IM':>7} "
f"{'n @maxloss':>11} {'n a budget 20%':>15} {'credito lordo$':>15}")
for f, m in res.items():
gg = scegli(rows, f, eseguibile=True)
ims = []
for g in gg:
s, lo = g["short"], g["long"]
if lo["ask"] is None:
continue
ims.append(im_short_put(s["S"], s["K"], usd(s["mark"], s), s) + usd(lo["ask"], lo))
if not ims:
continue
im, ml = float(np.median(ims)), m["ml"]
n_budget = math.floor(RISK_BUDGET * CAPITAL / ml) if ml > 0 else 0
print(f"{f:<10} {im:>14.2f} {ml:>10.2f} {CAPITAL/im:>7.1f} {CAPITAL/ml:>11.1f} "
f"{n_budget:>15} {n_budget*m['ce']:>15.2f}")
print("\n `IM corta+ala` = margine iniziale della sola put venduta (formula PUBBLICATA dal")
print(" venue, FONTE SECONDARIA) + il premio dell'ala, che si paga in contanti = il")
print(" fabbisogno se il venue NON netta le gambe. `max-loss` = il fabbisogno se LE NETTA.")
print(" ⚠️ Quale dei due valga NON e' leggibile dall'API pubblica: si chiude con")
print(" `private/get_margins` (lettura autenticata, fuori dal perimetro di questo file).")
print(" Ma la conclusione NON dipende da quale sia: il collaterale non e' il vincolo in")
print(" nessuna delle due letture.")
print(f" `n a budget 20%` = strutture che stanno in {RISK_BUDGET:.0%} del conto a max-loss;")
print(" `credito lordo` = il loro credito ESEGUIBILE sommato, per un ciclo. E' un incasso")
print(" LORDO PRIMA DI QUALUNQUE PERDITA, non un rendimento atteso: la struttura perde fino")
print(" a ~7x il credito quando va male (misura del progetto sul canonico BTC/ETH).")
def costo_raccolta(rows: list[dict], tab: dict) -> None:
print("\n" + "=" * 104)
print("5. COSTO DI RACCOLTA — cosa costerebbe aggiungere una famiglia al giro esistente")
print("=" * 104)
print(" `collect_chain.py` oggi: ~650 chiamate in ~160s a 4 rps (BTC+ETH inverse), al :25.")
print(f"{'famiglia':<10} {'a OI>=100':>10} {'a 2-lati':>9} {'+s a 4rps (2-lati)':>19} "
f"{'+% sul giro':>12}")
for f in LINEAR:
sub = [r for r in rows if r["fam"] == f]
two = sum(1 for r in sub if r["bid"] is not None and r["ask"] is not None)
print(f"{f:<10} {tab[f]['nliq']:>10} {two:>9} {(two+2)/4.0:>19.1f} "
f"{(two+2)/650*100:>11.0f}%")
print("\n ⚠️ Il vincolo non e' la CPU ma il rate limit Deribit PER-IP, gia' costato un guasto")
print(" il 29/07 (fallback silenzioso del feed 5m di SKH01). Ogni famiglia aggiunta allunga")
print(" la finestra in cui il giro occupa l'IP.")
# ---------------------------------------------------------------- main
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--cache", action="store_true", help="riusa lo snapshot su disco (niente rete)")
a = ap.parse_args()
print("=" * 104)
print("ALT-OPT — opzioni USDC-LINEARI su Deribit: c'e' una struttura eseguibile a $635?")
print("=" * 104)
print("Sola lettura pubblica, nessun ordine. UNO SNAPSHOT: qui non esiste un backtest.")
b, rete = Budget(), not a.cache
if a.cache and CACHE.exists():
raw = json.loads(CACHE.read_text())
print(f"\n[cache] {CACHE}")
else:
guardia_finestra()
t0 = time.time()
raw = fetch_all(b)
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(raw))
print(f"\n[rete] {b.calls} chiamate, {b.err} errori, {b.r429} 429, {time.time()-t0:.0f}s")
rows = build(raw)
print(f"[dato] {len(rows)} strumenti con specifiche E quote — snapshot "
f"{datetime.now(UTC):%Y-%m-%d %H:%M} UTC")
if not rows:
print("NESSUN DATO: il venue non ha risposto. Verdetto: non misurabile.")
return 1
unita(rows, b, rete)
tab = famiglie(rows)
liquidita(rows, tab)
res = struttura(rows)
vive = [f for f in res if res[f]["n"] >= 2]
if rete and vive:
guardia_finestra()
profondita(rows, vive, b)
else:
print("\n3b. PROFONDITA': non girata (--cache, oppure nessuna famiglia con >=2 strutture).")
collaterale(rows, res)
costo_raccolta(rows, tab)
print("\n" + "=" * 104)
print("6. COSA QUESTO FILE NON PUO' DIRE (dichiarato prima di qualunque verdetto)")
print("=" * 104)
print(" * Non c'e' STORIA: il collettore raccoglie BTC/ETH inverse, l'archivio ereditato")
print(" pure. Zero giorni di catena su queste famiglie -> nessun backtest, nessun hold-out,")
print(" nessun deflated-Sharpe. Il verdetto massimo ottenibile e' LEAD.")
print(" * Non c'e' IV-RANK: il gate>0.30 e' l'unico alpha misurato di VRP01 e richiede la")
print(" distribuzione storica della vol implicita del sottostante. Senza, la struttura non")
print(" e' VRP01: e' vendere vol a caso. (Su BTC/ETH quel gate e' passato 0/19 settimane.)")
print(" * Non c'e' STRESS nel campione: la regola 'niente short-vol da modello in deploy' e'")
print(" del 19/06 e la condizione dichiarata per rivalutarla e' un crash CATTURATO.")
print(" * Il margine e' da FORMULA PUBBLICATA, non dall'API, e il netting delle gambe non e'")
print(" leggibile pubblicamente.")
print(" * E' UN ISTANTE, per giunta di SABATO pomeriggio: uno spread relativo mediano a")
print(" un'ora del fine settimana non e' la sua distribuzione. Un book alt puo' essere")
print(" molto piu' quotato in settimana. Servirebbe almeno un ciclo settimanale.")
print(f"\n[rete, totale] {b.calls} chiamate, {b.err} errori, {b.r429} risposte 429")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+985
View File
@@ -0,0 +1,985 @@
#!/usr/bin/env python3
"""
r0822_basis_calendar.py — filone BASIS-CALENDAR (ondata 2026-08-22)
DOMANDA
Il progetto ha chiuso il FUNDING su tre lati (CC01 carry spot-perp, time-series,
cross-sectional) ma non ha mai guardato la STRUTTURA A TERMINE dei futures datati
di Deribit — l'unico strumento delta-neutro eseguibile sul NOSTRO venue, a 2 gambe.
Q1 Il basis (datato vs indice/perp) e la sua pendenza sono PREVEDIBILI, cioe'
esiste un roll-down incassabile, o e' un martingala rumoroso?
Q2 Un CALENDAR SPREAD (front vs back) su segnale di pendenza produce un edge
netto di fee e spread bid-ask REALI?
Q3 E' ortogonale al libro ed eseguibile a $600-3.000?
IL DATO — costruito qui, non esisteva
`data/raw/fut_*` sono futures INDICE AZIONARIO da IB (ES/NQ/DAX...), niente a che
vedere. Su Deribit non c'era nulla. Ma:
* /public/get_instruments?expired=true ritorna UN SOLO strumento (Deribit purga
subito le liste), quindi la via ovvia non funziona;
* i nomi dei trimestrali sono DETERMINISTICI (ultimo venerdi' di MAR/GIU/SET/DIC,
08:00 UTC) e `get_tradingview_chart_data` serve la storia COMPLETA di un
contratto SCADUTO se il nome lo si costruisce a mano.
Verificato: 30/30 trimestrali 2019Q1..2026Q2 rispondono `status=ok`.
=> **7+ anni di storia**, che includono il deleveraging 2022. E' esattamente la coda
che a CC01 mancava per costruzione (funding HL dal 2023-05): qui il 2022 c'e'.
La chiamata funding porta in regalo l'INDICE Deribit orario (`index_price`), quindi
spot e funding arrivano insieme e il basis si misura sull'indice vero, non su una proxy.
DISCIPLINA DI RETE (la VPS ha un IP solo e ci gira il libro)
<=2 richieste/s, e il fetcher DORME nella finestra :25-:29 di ogni ora, che e'
`cron_chain` (il collettore della catena opzioni). Il progetto ha gia' pagato un'ora
di guasto per un IP saturato. Tutto e' in cache su scratchpad: un ri-run non tocca la rete.
CONVENZIONE DI ROLL — dichiarata, e mai una serie incollata
I prezzi NON vengono mai splicciati. La posizione e' su una COPPIA DI CONTRATTI
specifica; al roll si chiude e si riapre pagando il round-trip pieno su entrambe le
gambe. L'incollaggio avviene solo nella serie dei RENDIMENTI, che e' l'unico posto in
cui e' lecito: e' l'errore che il progetto ha gia' commesso con le serie equity
ri-aggiustate, e qui e' strutturalmente impossibile.
USO
nice -n 19 timeout 900 uv run python scripts/research/r0822_basis_calendar.py
"""
from __future__ import annotations
import calendar
import datetime as dt
import json
import math
import os
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
SCRATCH = Path(
os.environ.get(
"R0822_SCRATCH",
"/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad",
)
)
CACHE = SCRATCH / "basis_cache"
CACHE.mkdir(parents=True, exist_ok=True)
API = "https://www.deribit.com/api/v2"
UTC = dt.timezone.utc
MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
ASSETS = ("BTC", "ETH")
FEE_SIDE = 0.00035 # 3,5 bps/lato per GAMBA (taker Deribit, letto dal venue)
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
HOURS_Y = 365.25 * 24
_last_call = [0.0]
# ==========================================================================
# 0. FETCHER — rate-limited, cache-first, consapevole dei cron della VPS
# ==========================================================================
def _polite_sleep() -> None:
"""<=2 req/s E fuori dalla finestra :25-:29 (cron_chain, stesso IP)."""
now = dt.datetime.now(UTC)
if 25 <= now.minute <= 29:
wait = (30 - now.minute) * 60 - now.second
print(f" [rete] finestra cron_chain (:25-:29) -> dormo {wait}s")
time.sleep(max(1, wait))
gap = time.time() - _last_call[0]
if gap < 0.55:
time.sleep(0.55 - gap)
_last_call[0] = time.time()
def _api(path: str, **kw) -> dict:
url = API + path + "?" + urllib.parse.urlencode(kw)
for attempt in range(4):
_polite_sleep()
try:
with urllib.request.urlopen(url, timeout=30) as r:
return json.load(r)
except Exception as exc: # noqa: BLE001
if attempt == 3:
raise
print(f" [rete] {type(exc).__name__} -> retry {attempt+1}/3")
time.sleep(2.0 * (attempt + 1))
raise RuntimeError("unreachable")
def last_friday(year: int, month: int) -> dt.date:
d = dt.date(year, month, calendar.monthrange(year, month)[1])
while d.weekday() != 4:
d -= dt.timedelta(days=1)
return d
def quarterly_universe(asset: str, y0: int = 2019, y1: int = 2027) -> list[tuple[str, pd.Timestamp]]:
"""Nomi DETERMINISTICI dei trimestrali Deribit: ultimo venerdi' di MAR/GIU/SET/DIC 08:00 UTC."""
out = []
for y in range(y0, y1 + 1):
for m in (3, 6, 9, 12):
lf = last_friday(y, m)
exp = pd.Timestamp(lf.year, lf.month, lf.day, 8, tz="UTC")
out.append((f"{asset}-{lf.day}{MONTHS[m-1]}{str(y)[2:]}", exp))
return out
def fetch_ohlc(instrument: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame:
"""OHLCV orario di UNO strumento (anche SCADUTO), a chunk di 90 giorni. Cache su disco."""
cf = CACHE / f"ohlc_{instrument.replace('/', '_')}.parquet"
if cf.exists():
return pd.read_parquet(cf)
frames = []
cur = t0
while cur < t1:
end = min(cur + pd.Timedelta(days=90), t1)
try:
res = _api("/public/get_tradingview_chart_data",
instrument_name=instrument, resolution="60",
start_timestamp=int(cur.timestamp() * 1000),
end_timestamp=int(end.timestamp() * 1000)).get("result", {})
except Exception as exc: # noqa: BLE001
print(f" {instrument}: chunk {cur.date()} FALLITO ({type(exc).__name__})")
cur = end
continue
ticks = res.get("ticks") or []
if res.get("status") == "ok" and ticks:
frames.append(pd.DataFrame({
"ts": pd.to_datetime(ticks, unit="ms", utc=True),
"open": res["open"], "high": res["high"],
"low": res["low"], "close": res["close"],
"volume": res.get("volume", [np.nan] * len(ticks)),
}))
cur = end
df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
if frames else pd.DataFrame(columns=["ts", "open", "high", "low", "close", "volume"]))
df.to_parquet(cf, index=False)
return df
def fetch_funding_and_index(asset: str, t0: pd.Timestamp, t1: pd.Timestamp) -> pd.DataFrame:
"""Funding orario del perp E indice Deribit (la chiamata li porta insieme)."""
cf = CACHE / f"fund_{asset}.parquet"
if cf.exists():
return pd.read_parquet(cf)
frames, cur = [], t0
while cur < t1:
end = min(cur + pd.Timedelta(days=30), t1)
try:
res = _api("/public/get_funding_rate_history",
instrument_name=f"{asset}-PERPETUAL",
start_timestamp=int(cur.timestamp() * 1000),
end_timestamp=int(end.timestamp() * 1000)).get("result", [])
except Exception: # noqa: BLE001
cur = end
continue
if res:
frames.append(pd.DataFrame({
"ts": pd.to_datetime([r["timestamp"] for r in res], unit="ms", utc=True),
"index": [r["index_price"] for r in res],
"f1h": [r["interest_1h"] for r in res],
}))
cur = end
df = (pd.concat(frames).drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
if frames else pd.DataFrame(columns=["ts", "index", "f1h"]))
df.to_parquet(cf, index=False)
return df
def fetch_live_books(instruments: list[str]) -> pd.DataFrame:
"""Calibrazione DATATA dello spread bid-ask: il book VIVO di oggi.
Non e' storia — e' un punto di ancoraggio per una stima, e va detto."""
cf = CACHE / "live_books.parquet"
if cf.exists():
return pd.read_parquet(cf)
rows = []
for nm in instruments:
try:
r = _api("/public/get_order_book", instrument_name=nm, depth=5)["result"]
bid, ask = r.get("best_bid_price"), r.get("best_ask_price")
if bid and ask and bid > 0:
rows.append(dict(instrument=nm, bid=bid, ask=ask,
mid=(bid + ask) / 2,
spread_bps=1e4 * (ask - bid) / ((ask + bid) / 2),
bid_sz=r.get("best_bid_amount"), ask_sz=r.get("best_ask_amount")))
except Exception: # noqa: BLE001
continue
df = pd.DataFrame(rows)
df.to_parquet(cf, index=False)
return df
# ==========================================================================
# 1. PANEL + CERTIFICAZIONE (prima di ogni strategia — regola di prim'ordine)
# ==========================================================================
def build_panel(asset: str) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Ritorna (wide, meta). `wide` = indice orario x contratto -> close.
`meta` = una riga per contratto con scadenza, barre, copertura, quota barre FLAT."""
uni = quarterly_universe(asset)
today = pd.Timestamp.now(tz="UTC")
closes, vols, meta = {}, {}, []
for nm, exp in uni:
if exp > today + pd.Timedelta(days=400):
continue
t0 = exp - pd.Timedelta(days=400)
t1 = min(exp + pd.Timedelta(hours=2), today)
if t1 <= t0:
continue
df = fetch_ohlc(nm, t0, t1)
if df.empty or len(df) < 200:
meta.append(dict(contract=nm, exp=exp, bars=len(df), status="VUOTO/CORTO"))
continue
s = df.set_index("ts")["close"].astype(float)
s = s[s > 0]
closes[nm] = s
vols[nm] = df.set_index("ts")["volume"].astype(float)
flat = float((s.diff() == 0).mean())
meta.append(dict(contract=nm, exp=exp, bars=len(s),
first=s.index.min(), last=s.index.max(),
flat_frac=flat, status="ok"))
wide = pd.DataFrame(closes).sort_index()
vwide = pd.DataFrame(vols).sort_index()
return wide, vwide, pd.DataFrame(meta)
def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFrame,
fund: pd.DataFrame, perp: pd.Series) -> dict:
"""Quattro difetti sorvegliati. Un edge su un book fermo NON e' un edge (regola 4)."""
print(f"\n --- CERTIFICAZIONE {asset} ---")
ok = meta[meta.status == "ok"]
print(f" contratti con dato: {len(ok)}/{len(meta)} "
f"finestra {wide.index.min().date()} .. {wide.index.max().date()}")
# (a) barre FLAT per bucket di giorni-a-scadenza = liquidita' vera del datato
rows = []
for nm in wide.columns:
exp = ok.loc[ok.contract == nm, "exp"]
if exp.empty:
continue
s = wide[nm].dropna()
dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0
flat = (s.diff() == 0).astype(float)
for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"),
(90, 180, "90-180g"), (180, 400, ">180g")]:
m = (dte >= lo) & (dte < hi)
if m.sum() > 50:
rows.append(dict(bucket=lab, flat=float(flat[m].mean()), n=int(m.sum())))
fb = pd.DataFrame(rows).groupby("bucket").agg(flat=("flat", "mean"), n=("n", "sum"))
order = ["0-30g", "30-90g", "90-180g", ">180g"]
print(" barre FLAT (proxy di illiquidita') per giorni-a-scadenza:")
for b in order:
if b in fb.index:
print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}")
# (b) volume orario per bucket (il flat da solo non basta).
# ATTENZIONE: il `volume` di get_tradingview_chart_data e' in VALUTA BASE (BTC/ETH),
# non in USD -> va moltiplicato per il prezzo, o si sbaglia di ~5 ordini di grandezza.
vrows = []
for nm in vwide.columns:
exp = ok.loc[ok.contract == nm, "exp"]
if exp.empty:
continue
v = vwide[nm].dropna()
px = wide[nm].reindex(v.index)
usd = (v * px).dropna() # <- conversione in USD
dte = (exp.iloc[0] - usd.index).total_seconds() / 86400.0
for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"),
(90, 180, "90-180g"), (180, 400, ">180g")]:
mm = (dte >= lo) & (dte < hi)
if mm.sum() > 50:
vrows.append(dict(bucket=lab, med=float(usd[mm].median()),
zero=float((usd[mm] == 0).mean())))
vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean"))
print(" volume orario mediano in USD (volume base x prezzo) e quota ore a volume ZERO:")
for b in order:
if b in vb.index:
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f}/ora ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
# (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere
conv = []
for nm in wide.columns:
e = ok.loc[ok.contract == nm, "exp"]
if e.empty:
continue
exp = e.iloc[0]
s = wide[nm].dropna()
tail = s[s.index >= exp - pd.Timedelta(hours=6)]
idx = fund.set_index("ts")["index"].reindex(tail.index, method="nearest", tolerance=pd.Timedelta("2h"))
if len(tail) and idx.notna().any():
conv.append(float(np.abs(np.log(tail / idx)).dropna().iloc[-1]) * 1e4)
if conv:
print(f" convergenza a scadenza |ln(F/indice)| ultima ora: "
f"mediana {np.median(conv):.1f} bps, max {np.max(conv):.1f} bps (n={len(conv)})")
# (c-bis) scarto PERP vs INDICE: la gamba FvP lo assume piccolo, quindi si misura
pj = perp.reindex(fund.set_index("ts").index).dropna()
if len(pj) > 1000:
ii = fund.set_index("ts")["index"].reindex(pj.index)
pb = (np.log(pj / ii) * 1e4).replace([np.inf, -np.inf], np.nan).dropna()
print(f" perp vs indice: mediana {pb.median():+.1f} bps, p95 |scarto| "
f"{pb.abs().quantile(0.95):.1f} bps (n={len(pb):,}) — la gamba FvP e' sul PERP")
# (d) CROSS-CHECK INDIPENDENTE: il forward implicito nella catena opzioni
xchk = cross_check_option_forward(asset, wide, ok)
return dict(flat=fb, vol=vb, conv=conv, xchk=xchk)
def cross_check_option_forward(asset: str, wide: pd.DataFrame, meta: pd.DataFrame) -> dict:
"""Il `underlying_price` di un'opzione Deribit E' il forward della SUA scadenza.
La catena raccolta dal 2026-07-30 e' quindi una MISURA INDIPENDENTE del basis:
se il mio F(T) e il forward delle opzioni sulla stessa scadenza non coincidono,
uno dei due e' sbagliato. (L'archivio ereditato da bite ha underlying_price NULL.)"""
import glob
files = sorted(glob.glob(str(PROJECT_ROOT / "data/raw/cb_chain/2026-*.parquet")))
if not files:
return dict(status="catena assente")
keep = []
for f in files[-30:]:
try:
d = pd.read_parquet(f, columns=["asset", "exp", "ts", "underlying_price", "index_price"])
except Exception: # noqa: BLE001
continue
d = d[(d.asset == asset) & d.underlying_price.notna()]
if len(d):
keep.append(d)
if not keep:
return dict(status="nessun underlying_price")
ch = pd.concat(keep)
ch["ts_h"] = ch["ts"].dt.floor("1h")
# forward mediano per (ora, scadenza) — tutti gli strike condividono lo stesso forward
fw = ch.groupby(["ts_h", "exp"]).agg(fwd=("underlying_price", "median")).reset_index()
rows = []
for nm in wide.columns:
e = meta.loc[meta.contract == nm, "exp"]
if e.empty:
continue
exp = e.iloc[0]
sub = fw[fw.exp == exp]
if sub.empty:
continue
mine = wide[nm].dropna()
j = sub.set_index("ts_h")["fwd"].reindex(mine.index).dropna()
if len(j) < 5:
continue
dev = (np.log(mine.reindex(j.index) / j) * 1e4).dropna()
rows.append(dict(contract=nm, n=len(dev), med_bps=float(dev.median()),
p95_bps=float(dev.abs().quantile(0.95))))
return dict(status="ok", table=pd.DataFrame(rows))
# ==========================================================================
# 2. CURVA — front/back trimestrali, basis annualizzato, slope forward
# ==========================================================================
def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame,
fund: pd.DataFrame, roll_dte: int,
perp: pd.Series | None = None) -> pd.DataFrame:
"""Per ogni ora: front = trimestrale piu' vicino con dte >= roll_dte, back = il successivo.
Ritorna prezzi, tau, basis annualizzati, slope forward e funding."""
ok = meta[meta.status == "ok"].sort_values("exp")
exps = {r.contract: r.exp for r in ok.itertuples()}
idx = wide.index
F = fund.set_index("ts").reindex(idx).ffill(limit=2)
n = len(idx)
order = [c for c in ok.contract if c in wide.columns] # ordine di SCADENZA
if len(order) < 2:
raise RuntimeError("meno di due contratti utilizzabili")
# ALIVE[i, k] = il k-esimo contratto (per scadenza) e' quotato a i E ha dte >= roll
TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order])
AV = np.column_stack([wide[c].notna().values for c in order])
ALIVE = AV & (TAU >= roll_dte * 24)
any1 = ALIVE.any(axis=1)
k1 = np.argmax(ALIVE, axis=1) # primo vivo = FRONT
A2 = ALIVE.copy()
A2[np.arange(n), k1] = False
any2 = A2.any(axis=1)
k2 = np.argmax(A2, axis=1) # secondo vivo = BACK
m = any1 & any2
front = np.where(m, np.array(order, dtype=object)[k1], "")
back = np.where(m, np.array(order, dtype=object)[k2], "")
rows = np.arange(n)
Wv = wide[order].values.astype(float)
Ff = np.where(m, Wv[rows, k1], np.nan)
Fb = np.where(m, Wv[rows, k2], np.nan)
tf = np.where(m, TAU[rows, k1], np.nan)
tb = np.where(m, TAU[rows, k2], np.nan)
out = pd.DataFrame(index=idx)
out["front"], out["back"] = front, back
out["F_front"], out["F_back"] = Ff, Fb
out["tau_front_h"], out["tau_back_h"] = tf, tb
out["index"] = F["index"].values
out["f1h"] = F["f1h"].values
# gamba perp: il PERP CERTIFICATO, non l'indice (l'indice non si compra).
# fallback all'indice solo se il perp non copre l'ora, e lo si dichiara.
if perp is not None:
pp = perp.reindex(idx)
out["perp"] = pp.where(pp.notna(), out["index"]).values
out["perp_from_index"] = pp.isna().values
else:
out["perp"] = out["index"].values
out["perp_from_index"] = True
# basis annualizzato di ciascuna gamba vs INDICE
out["c_front"] = np.log(Ff / out["index"]) * HOURS_Y / tf
out["c_back"] = np.log(Fb / out["index"]) * HOURS_Y / tb
# slope = tasso forward annualizzato FRA le due scadenze (stazionario, interpretabile)
out["slope"] = np.log(Fb / Ff) * HOURS_Y / (tb - tf)
out["s_raw"] = np.log(Fb / Ff)
return out
def describe_curve(asset: str, cv: pd.DataFrame) -> None:
"""Q1 — descrittiva. Il basis e' positivo? persiste? il suo CAMBIO e' prevedibile?"""
c = cv.dropna(subset=["c_front", "slope"])
print(f"\n --- Q1 CURVA {asset} (n={len(c):,} ore) ---")
print(f" basis annualizzato del FRONT: mediana {c.c_front.median()*100:+6.2f}%/anno "
f"media {c.c_front.mean()*100:+6.2f}% sd {c.c_front.std()*100:5.2f} "
f"contango {float((c.c_front>0).mean())*100:.1f}% del tempo")
print(f" slope forward front->back : mediana {c.slope.median()*100:+6.2f}%/anno "
f"media {c.slope.mean()*100:+6.2f}% sd {c.slope.std()*100:5.2f} "
f"positiva {float((c.slope>0).mean())*100:.1f}% del tempo")
fnd = cv["f1h"].dropna() * HOURS_Y
print(f" funding perp annualizzato : mediana {fnd.median()*100:+6.2f}%/anno "
f"media {fnd.mean()*100:+6.2f}%")
print(" per anno (mediane annualizzate):")
g = c.groupby(c.index.year).agg(basis_front=("c_front", "median"),
slope=("slope", "median"))
fy = (cv["f1h"] * HOURS_Y).groupby(cv.index.year).median()
for y, r in g.iterrows():
print(f" {y} basis_front {r.basis_front*100:+7.2f}% slope {r.slope*100:+7.2f}% "
f"funding {fy.get(y, np.nan)*100:+7.2f}%")
# prevedibilita': mean-reversion dello slope a 7 giorni (AR sul livello)
for h, lab in [(24, "1g"), (24 * 7, "7g"), (24 * 30, "30g")]:
x = c["slope"]
dx = x.shift(-h) - x
j = pd.concat([x, dx], axis=1).dropna()
if len(j) > 500:
b = np.polyfit(j.iloc[:, 0], j.iloc[:, 1], 1)[0]
rho = float(j.iloc[:, 0].corr(j.iloc[:, 1]))
print(f" mean-reversion slope a {lab:>3s}: beta {b:+.3f} corr {rho:+.3f} "
f"(beta -1 = reversione totale, 0 = martingala)")
def basis_vs_funding(asset: str, cv: pd.DataFrame) -> None:
"""Q1, nella forma decidibile: il basis del front e' il premio ANNUALIZZATO che
incassi se vendi il datato e lo tieni a scadenza. La copertura e' il perp, su cui
PAGHI il funding realizzato nello stesso periodo. Quindi il roll-down incassabile
NON e' il basis: e' `basis - funding realizzato fino a scadenza`.
Se il basis e' un previsore NON DISTORTO del funding (a~0, b~1) il premio a termine
esiste come LIVELLO ma non e' estraibile: e' gia' il prezzo della copertura."""
d = cv[["c_front", "f1h", "tau_front_h"]].dropna()
if len(d) < 5000:
print(f" {asset}: campione insufficiente")
return
f = d["f1h"].values
cum = np.concatenate([[0.0], np.cumsum(f)]) # somma cumulata del funding orario
n = len(d)
tau = d["tau_front_h"].values
end = np.minimum(np.arange(n) + tau.astype(int), n - 1)
hrs = np.maximum(end - np.arange(n), 1)
realized = (cum[end] - cum[np.arange(n)]) / hrs * HOURS_Y # funding realizzato ANNUALIZZATO
implied = d["c_front"].values
m = np.isfinite(realized) & np.isfinite(implied) & (hrs > 24 * 5)
x, y = implied[m], realized[m]
b1, b0 = np.polyfit(x, y, 1)
prem = x - y # premio effettivamente incassabile
# IC bootstrap a BLOCCHI (le ore sono fortemente autocorrelate: un IC iid mentirebbe)
rng = np.random.default_rng(822)
bl = 24 * 30
nb = max(1, len(prem) // bl)
boots = []
for _ in range(400):
st = rng.integers(0, max(1, len(prem) - bl), nb)
boots.append(np.mean(np.concatenate([prem[i:i + bl] for i in st])))
lo, hi = np.percentile(boots, [2.5, 97.5])
print(f" {asset}: basis implicito medio {np.mean(x)*100:+6.2f}%/anno "
f"funding realizzato a scadenza {np.mean(y)*100:+6.2f}%/anno")
print(f" regressione realizzato ~ a + b*implicito: b = {b1:+.3f} a = {b0*100:+.2f}%"
f" (b~1 e a~0 = previsore non distorto => niente da incassare)")
print(f" PREMIO INCASSABILE = implicito - realizzato = {np.mean(prem)*100:+6.2f}%/anno"
f" IC95% a blocchi [{lo*100:+.2f}%, {hi*100:+.2f}%] (n={m.sum():,} ore)")
# ATTENZIONE: annualizzare dividendo per tau ESPLODE il rumore a tau piccolo
# (5 bps di rumore a 7 giorni dalla scadenza diventano +2.6%/anno). Media per bucket:
tt = tau[m] / 24.0
print(" premio per giorni-a-scadenza (l'annualizzazione a tau piccolo amplifica il rumore):")
for lo_d, hi_d, lab in [(5, 30, " 5-30g"), (30, 60, " 30-60g"), (60, 100, "60-100g")]:
k = (tt >= lo_d) & (tt < hi_d)
if k.sum() > 500:
print(f" {lab}: implicito {np.mean(x[k])*100:+6.2f}% realizzato "
f"{np.mean(y[k])*100:+6.2f}% premio {np.mean(prem[k])*100:+6.2f}%/anno "
f"(sd {np.std(prem[k])*100:5.1f}, n={k.sum():,})")
# ==========================================================================
# 3. STRATEGIE — posizioni in spazio CONTRATTO (il roll si paga da solo)
# ==========================================================================
def _z(x: pd.Series, win: int) -> pd.Series:
m = x.rolling(win, min_periods=win // 3).mean()
s = x.rolling(win, min_periods=win // 3).std()
return ((x - m) / s.replace(0, np.nan))
def asset_ctx(wide: pd.DataFrame) -> dict:
"""Precalcolo per asset: matrice dei rendimenti per contratto + mappa colonne.
Fatto UNA volta e riusato da tutte le celle (altrimenti la griglia non sta nel budget)."""
Wv = wide.values.astype(float)
R = np.zeros_like(Wv)
R[1:] = Wv[1:] / Wv[:-1] - 1.0
R[~np.isfinite(R)] = 0.0
return dict(R=R, cpos={c: k for k, c in enumerate(wide.columns)},
ncol=Wv.shape[1], idx=wide.index)
def pair_idx(cv: pd.DataFrame, ctx: dict) -> tuple[np.ndarray, np.ndarray]:
"""Indici di colonna di front/back per ogni ora (-1 = coppia non disponibile)."""
cp = ctx["cpos"]
fi = np.array([cp.get(x, -1) for x in cv["front"].values], dtype=int)
bi = np.array([cp.get(x, -1) for x in cv["back"].values], dtype=int)
return fi, bi
def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) -> np.ndarray:
base = cv["slope"] if family == "CAL" else cv["c_front"]
if sig == "static_long":
w = pd.Series(1.0, index=cv.index)
elif sig == "static_short":
w = pd.Series(-1.0, index=cv.index)
elif sig == "carry": # contango -> vendi il datato
w = -np.sign(base)
elif sig in ("zrev", "zmom"):
z = _z(base, win_h)
w = (-np.tanh(z)) if sig == "zrev" else np.tanh(z)
w = w.where(z.abs() >= thr, 0.0)
else:
raise ValueError(sig)
return np.nan_to_num(np.asarray(w, float), nan=0.0).clip(-1, 1)
def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
win_h: int, thr: float, slip_bps: float,
fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1,
dec_hour: int | None = None) -> pd.Series:
"""Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA.
Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione
per contratto e' automaticamente piena => il roll paga il round-trip completo su
entrambe le gambe. Nessuna serie di prezzi viene MAI incollata: si incollano solo i
RENDIMENTI, che e' l'unico posto in cui e' lecito.
Causalita': w e' deciso con dati <= t e TENUTO durante t -> t+1 (shift esplicito qui,
non nel segnale) => non si puo' sbagliare per distrazione a valle."""
idx = ctx["idx"]
n, ncol = len(idx), ctx["ncol"]
if fi is None or bi is None:
fi, bi = pair_idx(cv, ctx)
w = signal_w(cv, family, sig, win_h, thr)
if dec_hour is not None:
# ANCORA: la posizione si aggiorna SOLO a quell'ora del giorno e si TIENE il resto
# (non "si guadagna solo a quell'ora", che sarebbe un'altra strategia).
ws = pd.Series(w, index=idx).where(idx.hour == dec_hour)
w = ws.ffill().fillna(0.0).values
P = np.zeros((n, ncol))
rows = np.arange(n)
if family == "CAL":
m = (fi >= 0) & (bi >= 0)
P[rows[m], bi[m]] = w[m] # long spread = long back
P[rows[m], fi[m]] = -w[m] # / short front
else: # FvP: solo la gamba datata
m = fi >= 0
P[rows[m], fi[m]] = w[m]
held = np.zeros_like(P)
held[lag:] = P[:-lag] if lag else P # deciso a t, tenuto in t->t+lag
gross = (held * ctx["R"]).sum(axis=1)
dP = np.zeros_like(P)
dP[1:] = P[1:] - P[:-1]
dP[0] = P[0]
turn = np.abs(dP).sum(axis=1)
if family == "FvP":
perp_r = np.nan_to_num(cv["perp"].pct_change().values, nan=0.0)
f1h = np.nan_to_num(cv["f1h"].values, nan=0.0)
wheld = held.sum(axis=1) # una sola gamba datata
# long datato => short perp: guadagna -w*perp_r e INCASSA w*funding
gross = gross - wheld * perp_r + wheld * f1h
turn = turn * 2.0 # datato + gamba perp
cost = turn * (fee_side + slip_bps * 1e-4)
return pd.Series(gross - cost, index=idx).fillna(0.0)
# ==========================================================================
# 4. METRICHE / GATE
# ==========================================================================
def combine(hs: list[pd.Series]) -> pd.Series:
"""50/50 BTC+ETH sulle ore COMUNI (inner join, come tp01_baseline_daily)."""
J = pd.concat(hs, axis=1, join="inner").fillna(0.0)
return J.mean(axis=1)
def to_daily(h: pd.Series) -> pd.Series:
return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna()
def stats(daily: pd.Series) -> dict:
r = daily.dropna()
if len(r) < 30 or r.std() == 0:
return dict(sharpe=0.0, holdout=0.0, maxdd=0.0, cagr=0.0, n=len(r), vol=0.0)
sh = float(r.mean() / r.std() * math.sqrt(365.25))
eq = (1 + r).cumprod()
dd = float(((eq.cummax() - eq) / eq.cummax()).max())
yrs = (r.index[-1] - r.index[0]).days / 365.25
cagr = float(eq.iloc[-1] ** (1 / yrs) - 1) if yrs > 0.5 and eq.iloc[-1] > 0 else float("nan")
ho = r[r.index >= HOLDOUT]
sho = float(ho.mean() / ho.std() * math.sqrt(365.25)) if len(ho) > 30 and ho.std() > 0 else float("nan")
yrs_ = max(len(r) / 365.25, 1e-9)
return dict(sharpe=sh, holdout=sho, maxdd=dd, cagr=cagr, n=len(r),
vol=float(r.std() * math.sqrt(365.25)),
se_ann=float(r.std() * math.sqrt(365.25) / math.sqrt(yrs_)))
def delevering_null(cand: pd.Series, base: pd.Series) -> dict:
"""NULL DEL DE-LEVERING (5 occorrenze in questo progetto). Esiste k<1 che,
applicato al BASELINE, da' lo STESSO maxDD del candidato con Sharpe MIGLIORE?
Se si', il candidato non protegge: e' solo meno leva."""
cs = stats(cand)
best = None
for k in np.arange(0.05, 1.01, 0.05):
s = stats(base * k)
if s["maxdd"] <= cs["maxdd"] + 1e-9:
best = (float(k), s)
break
if best is None:
return dict(refuted=False, note="nessun k<=1 raggiunge quel DD sul baseline")
k, s = best
return dict(refuted=bool(s["sharpe"] > cs["sharpe"]), k=k,
base_sharpe_at_k=s["sharpe"], cand_sharpe=cs["sharpe"],
base_dd_at_k=s["maxdd"], cand_dd=cs["maxdd"])
# ==========================================================================
# 5. MAIN
# ==========================================================================
def main() -> None:
t_start = time.time()
print("=" * 78)
print("r0822 BASIS-CALENDAR — struttura a termine dei futures datati Deribit")
print("=" * 78)
today = pd.Timestamp.now(tz="UTC")
panels, funds, metas, vwides = {}, {}, {}, {}
# ---------------- STEP 1: dato ----------------
print("\n[1] DATO — ricostruzione da Deribit pubblico (cache su scratchpad)")
for a in ASSETS:
print(f" {a}: trimestrali (ultimo venerdi' MAR/GIU/SET/DIC 08:00 UTC)...")
wide, vwide, meta = build_panel(a)
fund = fetch_funding_and_index(a, pd.Timestamp("2019-01-01", tz="UTC"), today)
panels[a], vwides[a], metas[a], funds[a] = wide, vwide, meta, fund
ok = meta[meta.status == "ok"]
print(f" {len(ok)} contratti, {int(meta.bars.sum()):,} barre orarie, "
f"funding+indice {len(fund):,} ore")
# ---------------- STEP 2: certificazione ----------------
print("\n[2] CERTIFICAZIONE (regola 4: un edge su un book fermo non e' un edge)")
certs, perps = {}, {}
for a in ASSETS:
pdf = A.get(a, "1h")
perps[a] = pd.Series(
pdf["close"].astype(float).values,
index=pd.DatetimeIndex(pd.to_datetime(pdf["datetime"], utc=True))).sort_index()
perps[a] = perps[a][~perps[a].index.duplicated()]
certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perps[a])
x = certs[a]["xchk"]
if x.get("status") == "ok" and len(x["table"]):
t = x["table"]
print(f" CROSS-CHECK INDIPENDENTE vs forward della catena opzioni "
f"({int(t.n.sum())} ore, {len(t)} scadenze):")
print(f" deviazione mediana {t.med_bps.median():+.1f} bps, "
f"p95 |dev| {t.p95_bps.max():.1f} bps")
else:
print(f" CROSS-CHECK catena: {x.get('status')}")
# ---------------- STEP 3: Q1 curva ----------------
print("\n[3] Q1 — LA CURVA E' PREVEDIBILE?")
curves = {}
for a in ASSETS:
cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7, perp=perps[a])
curves[a] = cv
describe_curve(a, cv)
print("\n --- Q1-bis: il basis e' un PREVISORE del funding che paghi sulla copertura? ---")
for a in ASSETS:
basis_vs_funding(a, curves[a])
# ---------------- STEP 3b: spread bid-ask (calibrazione DATATA) ----------------
print("\n[3b] SPREAD BID-ASK — calibrazione sul book VIVO di oggi (non e' storia)")
live = []
for a in ASSETS:
for nm, exp in quarterly_universe(a):
if today < exp <= today + pd.Timedelta(days=400):
live.append(nm)
lb = fetch_live_books(live + [f"{a}-PERPETUAL" for a in ASSETS])
slip_est = 5.0
if len(lb):
for r in lb.itertuples():
print(f" {r.instrument:16s} bid {r.bid:>10,.1f} ask {r.ask:>10,.1f} "
f"spread {r.spread_bps:6.1f} bps size {r.bid_sz:>8,.0f}/{r.ask_sz:>8,.0f}")
dated = lb[~lb.instrument.str.contains("PERPETUAL")]
if len(dated):
slip_est = float(dated.spread_bps.median()) / 2.0
print(f" => mezzo-spread MEDIANO del datato = {slip_est:.1f} bps/lato "
f"(STIMA datata oggi, NON storia: il book storico dei datati non e' disponibile)")
# ---------------- STEP 4: griglia ----------------
print("\n[4] GRIGLIA — dichiarata PRIMA, contata AL RIALZO")
FAM = {"CAL": ["static_long", "static_short", "zrev", "zmom"],
"FvP": ["carry", "static_long", "static_short", "zrev", "zmom"]}
WINS = [720, 2160, 4320] # 30 / 90 / 180 giorni in ore
ROLLS = [7, 21]
THRS = [0.0, 1.0]
cells = [(f, s, w, rl, th) for f, sigs in FAM.items() for s in sigs
for w in WINS for rl in ROLLS for th in THRS]
print(f" famiglie {list(FAM)} x segnali x finestre {WINS} x roll {ROLLS} x soglie {THRS}")
print(f" = {len(cells)} celle valutate (le varianti statiche non usano W/soglia:"
f" le conto lo stesso, al rialzo)")
ctxs = {a: asset_ctx(panels[a]) for a in ASSETS}
cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl, perp=perps[a])
for a in ASSETS for rl in ROLLS}
pidx = {(a, rl): pair_idx(cvcache[(a, rl)], ctxs[a]) for a in ASSETS for rl in ROLLS}
rows = []
for (fam, sig, win, rl, th) in cells:
hs = []
for a in ASSETS:
f_i, b_i = pidx[(a, rl)]
h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est,
fi=f_i, bi=b_i)
hs.append(h)
d = to_daily(combine(hs))
st = stats(d)
rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th,
daily=d, **st))
G = pd.DataFrame([{k: v for k, v in r.items() if k != "daily"} for r in rows])
dailies = {i: r["daily"] for i, r in enumerate(rows)}
print(f"\n distribuzione dello Sharpe FULL sulle {len(G)} celle: "
f"min {G.sharpe.min():+.2f} p25 {G.sharpe.quantile(.25):+.2f} "
f"mediana {G.sharpe.median():+.2f} p75 {G.sharpe.quantile(.75):+.2f} "
f"max {G.sharpe.max():+.2f} sd {G.sharpe.std():.2f}")
degen = G[(G.vol < 0.005) | (G.n < 400)]
if len(degen):
print(f" ATTENZIONE: {len(degen)} celle DEGENERI (vol<0.5%/anno o <400 giorni attivi):")
print(f" Sharpe di quelle celle: {sorted(np.round(degen.sharpe.values,2))[:10]}")
print(" gonfiano la varianza dei trial e quindi il null del deflated-Sharpe:")
print(" il DSR viene riportato ANCHE sulla sola sottogriglia non degenere.")
print("\n MIGLIORE CELLA PER FAMIGLIA (Q2 riguarda il CALENDAR: va letto separato):")
for fam in FAM:
sub = G[G.family == fam]
i = sub.sharpe.idxmax()
r = G.loc[i]
print(f" [{fam}] {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g thr={r.thr:.1f} "
f"| Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} DD {r.maxdd*100:5.2f}% "
f"CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%")
ii = A.implausible_sharpe(dailies[i])
print(f" implausible_sharpe={ii['implausible']} "
f"(barre in perdita {ii.get('loss_frac', float('nan'))*100:.1f}%) "
f"-> {'coda ASSENTE, rischio fuori dal campione' if ii['implausible'] else 'coda PRESENTE'}")
print("\n LE CELLE CANONICHE (quelle che un lettore chiede per nome), roll=7g:")
for fam, sig, lab in [
("FvP", "carry", "vendi il datato quando e' in CONTANGO / compra perp (il carry classico)"),
("FvP", "static_short", "SEMPRE corto il datato / lungo perp = LA RACCOLTA DEL CONTANGO"),
("FvP", "static_long", "SEMPRE lungo il datato / corto perp (il contrario)"),
("CAL", "static_long", "calendar: lungo back / corto front"),
("CAL", "static_short", "calendar: corto back / lungo front")]:
sub = G[(G.family == fam) & (G.sig == sig) & (G.roll == 7)]
if len(sub):
r = G.loc[sub.index[0]]
print(f" {fam}/{sig:12s} Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} "
f"CAGR {r.cagr*100:+6.2f}% (+-{r.se_ann*100:.2f} SE) DD {r.maxdd*100:5.2f}% "
f"vol {r.vol*100:5.2f}% <- {lab}")
print("\n migliori 8 celle per Sharpe FULL (NON e' la selezione — solo panorama):")
for i in G.sharpe.nlargest(8).index:
r = G.loc[i]
print(f" {r.family:4s} {r.sig:12s} W={int(r.win):5d}h roll={int(r.roll):2d}g "
f"thr={r.thr:.1f} | Sh {r.sharpe:+6.2f} hold {r.holdout:+6.2f} "
f"DD {r.maxdd*100:5.2f}% CAGR {r.cagr*100:+6.2f}% vol {r.vol*100:5.2f}%")
# ---------------- STEP 5: selezione IN-SAMPLE ONLY ----------------
print("\n[5] SELEZIONE IN-SAMPLE-ONLY (mai la cella col miglior hold-out)")
ins = {}
for i, d in dailies.items():
pre = d[d.index < HOLDOUT]
ins[i] = (float(pre.mean() / pre.std() * math.sqrt(365.25))
if len(pre) > 60 and pre.std() > 0 else -9.9)
best_i = max(ins, key=ins.get)
b = G.loc[best_i]
bd = dailies[best_i]
bs = stats(bd)
print(f" cella scelta AL BUIO: {b.family} {b.sig} W={int(b.win)}h roll={int(b.roll)}g thr={b.thr}")
print(f" Sharpe in-sample {ins[best_i]:+.2f} -> FULL {bs['sharpe']:+.2f} "
f"HOLD-OUT {bs['holdout']:+.2f} maxDD {bs['maxdd']*100:.2f}% "
f"CAGR {bs['cagr']*100:+.2f}% (+-{bs['se_ann']*100:.2f} SE annuo) "
f"vol {bs['vol']*100:.2f}%")
print(f" NB con {bs['n']/365.25:.1f} anni e vol {bs['vol']*100:.1f}% l'errore standard del "
f"rendimento annuo e' {bs['se_ann']*100:.2f}pp: sotto ~{2*bs['se_ann']*100:.1f}pp/anno "
f"NIENTE e' distinguibile da zero, ne' in un verso ne' nell'altro.")
best_full = G.sharpe.idxmax()
print(f" (per confronto, la cella col miglior FULL sarebbe stata "
f"{G.loc[best_full,'family']} {G.loc[best_full,'sig']} "
f"Sh {G.loc[best_full,'sharpe']:+.2f} — NON e' quella scelta)")
# ---------------- STEP 6: GATE ----------------
print("\n[6] GATE")
dsr, sr0 = A.deflated_sharpe(bs["sharpe"], list(G.sharpe.values), bd)
print(f" deflated-Sharpe : DSR {dsr:.3f} (null max atteso {sr0:+.2f} su {len(G)} trial) "
f"-> {'PASS' if dsr >= 0.95 else 'FAIL'}")
nd = G[(G.vol >= 0.005) & (G.n >= 400)]
if len(nd) >= 5 and len(nd) < len(G):
d2, s2_ = A.deflated_sharpe(bs["sharpe"], list(nd.sharpe.values), bd)
print(f" sulla sola sottogriglia NON degenere ({len(nd)} trial): "
f"DSR {d2:.3f} (null max {s2_:+.2f}) -> {'PASS' if d2 >= 0.95 else 'FAIL'}")
imp = A.implausible_sharpe(bd)
print(f" implausible_sharpe: implausible={imp['implausible']} "
f"Sh {imp.get('sharpe', float('nan')):+.2f} maxDD {imp.get('maxdd', float('nan'))*100:.2f}% "
f"quota barre in perdita {imp.get('loss_frac', float('nan'))*100:.1f}% "
f"Calmar {imp.get('calmar', float('nan')):.1f}")
if imp["reasons"]:
for rr in imp["reasons"]:
print(f" - {rr}")
tp = A.tp01_baseline_daily()
try:
mg = A.marginal_vs_tp01(bd)
print(f" marginal_vs_tp01 : {mg.get('marginal_verdict')} "
f"corr_full {mg.get('corr_full')} corr_hold {mg.get('corr_hold')} "
f"robust_oos={mg.get('robust_oos')} insample_edge={mg.get('has_insample_edge')} "
f"is_hedge={mg.get('is_hedge')} beats_noise={mg.get('beats_noise_null')}")
w25 = mg.get("w25") or (mg.get("blends", {}) or {}).get("w25", {})
print(f" uplift blend w=25%: full {w25.get('uplift_full')} "
f"hold {w25.get('uplift_hold')}")
except Exception as exc: # noqa: BLE001
mg = {}
print(f" marginal_vs_tp01 : NON GIRATO ({type(exc).__name__}: {exc})")
dn = delevering_null(bd, tp)
print(f" null de-levering : refuted={dn.get('refuted')} {dn}")
# anchor: l'ora del giorno in cui si valuta il segnale
print(" anchor_luck_band (ora del giorno di decisione, 24 ancore):")
def by_off(off):
hs = []
for a in ASSETS:
f_i, b_i = pidx[(a, int(b.roll))]
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
int(b.win), float(b.thr), slip_est,
fi=f_i, bi=b_i, dec_hour=off))
return to_daily(combine(hs))
try:
ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0)
if "median" in ab:
print(f" canonica {ab['canonical']:+.3f} (pctl {ab['canonical_pctl']*100:.0f}°) "
f"MEDIANA(stima onesta) {ab['median']:+.3f} banda [{ab['lo']:+.3f},{ab['hi']:+.3f}] "
f"frazione>0 {ab['frac_positive']:.2f} fortuna {ab['luck']:+.3f} "
f"gate_pass={ab['gate_pass']}")
else:
print(f" {ab}")
except Exception as exc: # noqa: BLE001
print(f" NON GIRATO ({type(exc).__name__}: {exc})")
try:
print(f" causality_ok : la posizione e' shiftata in run_strategy "
f"(held = pos.shift(1)); verifica strutturale, non A.causality_ok "
f"(che vuole un target_fn su BTC/ETH direzionale)")
except Exception: # noqa: BLE001
pass
# ---------------- STEP 6b: RITARDO D'ESECUZIONE (il test decisivo qui) ----------------
print("\n[6b] RITARDO D'ESECUZIONE — il test decisivo di questo filone")
print(" Un datato lontano ha prezzi STANTII: una mean-reversion su una serie stantia")
print(" mostra profitti FINTI (il prezzo 'reverte' solo perche' si aggiorna in ritardo).")
print(" Un edge di STRUTTURA A TERMINE e' lento e sopravvive a un'ora in piu' di ritardo;")
print(" un artefatto di microstruttura muore. (Stessa lente del decadimento di XSR01.)")
lag_tab = []
for lg in [1, 2, 3, 6, 12, 24]:
hs = []
for a in ASSETS:
f_i, b_i = pidx[(a, int(b.roll))]
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i, lag=lg))
sl_ = stats(to_daily(combine(hs)))
lag_tab.append((lg, sl_["sharpe"], sl_["cagr"]))
print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%")
s1 = lag_tab[0][1]; s2 = lag_tab[1][1]
if s1 > 0:
keep = s2 / s1
print(f" con UNA sola ora in piu' resta il {keep*100:.0f}% dello Sharpe "
f"({s1:+.2f} -> {s2:+.2f})")
print(" -> " + ("compatibile con un edge lento di struttura a termine"
if keep > 0.6 else
"CROLLO: firma di microstruttura/prezzo stantio, NON di term structure"))
# ---------------- STEP 7: quanto spread uccide l'edge ----------------
print("\n[7] A CHE SPREAD MUORE L'EDGE (la stima di slippage e' il rischio #1)")
for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]:
hs = []
for a in ASSETS:
f_i, b_i = pidx[(a, int(b.roll))]
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family,
b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i))
s2 = stats(to_daily(combine(hs)))
mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else ""
print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} "
f"CAGR {s2['cagr']*100:+6.2f}%{mark}")
# ---------------- STEP 8: eseguibilita' ----------------
print("\n[8] Q3 — ESEGUIBILITA' a $600-3.000")
print(" lotti minimi letti dal VENUE (non da una tabella cablata):")
px = {a: float(curves[a]["index"].dropna().iloc[-1]) for a in ASSETS}
for a in ASSETS:
for nm in (f"{a}-25DEC26", f"{a}_USDC-25DEC26"):
try:
sp = _api("/public/get_instrument", instrument_name=nm)["result"]
lin = sp.get("settlement_currency") == "USDC"
lot = (sp["min_trade_amount"] * px[a]) if lin else sp["min_trade_amount"]
print(f" {nm:22s} {'LINEARE' if lin else 'INVERSE':8s} "
f"min {sp['min_trade_amount']:<10.4g} = ${lot:>8,.2f} di nozionale "
f"tick {sp['tick_size']} taker {sp['taker_commission']*1e4:.1f} bps")
except Exception as exc: # noqa: BLE001
print(f" {nm:22s} specs non lette ({type(exc).__name__})")
print(" (il libro live trada i LINEARI USDC; i trimestrali studiati qui sono gli INVERSE,")
print(" che hanno 7 anni di storia. Le due famiglie hanno lotti minimi DIVERSI.)")
print(f" vol ANNUA del candidato per $1 di nozionale lordo/gamba: {bs['vol']*100:.2f}%")
if bs["vol"] > 0:
lev = 0.20 / bs["vol"]
print(f" per portarlo al vol-target 20% del progetto servirebbe leva "
f"{lev:.1f}x per gamba = ${lev*2:,.0f} di nozionale LORDO per $1 di conto")
for cap in (600, 3000):
print(f" a ${cap}: nozionale lordo ${lev*2*cap:,.0f} "
f"(margine ~2-4%/gamba => ${lev*2*cap*0.03:,.0f} di IM richiesto)")
print(f"\n[fine] {time.time()-t_start:.0f}s")
if __name__ == "__main__":
main()
+691
View File
@@ -0,0 +1,691 @@
"""r0822 — DEALER GAMMA: il regime di gamma dei dealer separa mean-reversion da trend su BTC/ETH?
Filone dell'ondata 2026-08-22. Dato mai usato dal progetto: `data/raw/cb_market_snapshots.parquet`
(ereditato da cerbero-bite, dismesso il 30/07) con `dealer_net_gamma`, `gamma_flip_level`,
`oi_delta_pct_4h`, `liquidation_*_risk`.
IPOTESI DI MERCATO CLASSICA (due gambe, vanno confermate ENTRAMBE):
(A) dealer LONG gamma -> l'hedging SOPPRIME la vol realizzata; SHORT gamma -> la AMPLIFICA;
(B) dealer LONG gamma -> il prezzo MEAN-REVERTE; SHORT gamma -> il prezzo TRENDA.
La gamba tradeable e' la (B): e' quella che diventerebbe un gate di regime sopra TP01/SKH01.
Aspettativa dichiarata PRIMA di misurare: (A) plausibile ma confusa con il livello di vol;
(B) improbabile su 3 mesi, e comunque a rischio "TP01 travestito".
ORDINE OBBLIGATO: prima la validazione del DATO (non e' certificato, la fonte non esiste piu'),
poi il FATTO condizionale, e solo dopo — se il fatto c'e' — il gate.
Girare: nice -n 19 timeout 900 uv run python scripts/research/r0822_dealer_gamma.py
"""
from __future__ import annotations
import glob
import sys
import warnings
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" / "alt"))
import altlib as A # noqa: E402
warnings.filterwarnings("ignore")
pd.set_option("display.width", 220)
RAW = ROOT / "data" / "raw"
SNAP_F = RAW / "cb_market_snapshots.parquet"
CHAIN_D = RAW / "cb_chain"
SEED = 20260822
ASSETS = ("BTC", "ETH")
HOUR_YEAR = 24 * 365.0
ANN = np.sqrt(HOUR_YEAR) * 100.0 # da dev.std. oraria a vol annua in punti percentuali
# La cadenza a 15 min comincia qui: prima ci sono 37 punti GIORNALIERI (vedi sezione 1).
HF_START = pd.Timestamp("2026-05-01 15:00", tz="UTC")
# ------------------------------------------------------------------ conteggio dei trial
# Ogni cella/statistica valutata viene contata qui, AL RIALZO (regola: il conteggio dei trial
# e' l'unico posto dove barare e' indolore e invisibile — 30/07). Il verdetto stampa la
# ripartizione DERIVATA da questo dizionario, non una somma scritta a mano.
COUNT: dict[str, int] = dict(ricostruzione=0, sma=0, regressioni=0, vol_condizionata=0,
variance_ratio=0, momentum=0, gate=0, delevering=0)
NEFF: dict[str, float] = {} # ampiezza effettiva per asset, misurata in sezione 1e
def hr(t: str) -> None:
print("\n" + "=" * 96 + f"\n{t}\n" + "=" * 96)
def sh_d(x: pd.Series) -> float:
x = pd.Series(x).dropna()
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 2 and x.std() > 0 else 0.0
def dd_d(x: pd.Series) -> float:
e = (1.0 + pd.Series(x).dropna()).cumprod()
return float((e / e.cummax() - 1.0).min()) if len(e) else 0.0
# ------------------------------------------------------------------ caricamento
def load_snap() -> pd.DataFrame:
d = pd.read_parquet(SNAP_F).sort_values("ts")
d["ts"] = pd.to_datetime(d["ts"], utc=True).astype("datetime64[ns, UTC]")
return d
def load_bars(asset: str) -> pd.DataFrame:
px = A.get(asset, "1h").copy()
px["dt"] = pd.to_datetime(px["timestamp"], unit="ms", utc=True).astype("datetime64[ns, UTC]")
px = px[["dt", "close"]].dropna().reset_index(drop=True)
px["r"] = np.log(px["close"]).diff()
return px
def joined(asset: str, snap: pd.DataFrame) -> pd.DataFrame:
"""Barra oraria + ULTIMO snapshot con ts <= apertura della barra.
Causale per costruzione: `direction='backward'` sull'apertura della barra, quindi il
regime usato per la barra [t, t+1h) e' noto a t. Tolleranza 2h: se il collettore e'
fermo da piu' di due ore la riga esce, invece di trascinare un valore vecchio.
"""
px = load_bars(asset)
s = (snap[snap["asset"] == asset]
[["ts", "dealer_net_gamma", "gamma_flip_level", "spot", "dvol", "realized_vol_30d"]]
.dropna(subset=["dealer_net_gamma", "gamma_flip_level"]))
m = pd.merge_asof(px.sort_values("dt"), s.sort_values("ts"),
left_on="dt", right_on="ts", direction="backward",
tolerance=pd.Timedelta("2h"))
m = m[m["ts"].notna()].reset_index(drop=True)
# SEGNO CORRETTO (giustificato nella sezione 2): dealer_net_gamma = -GEX
# dealer_net_gamma > 0 <=> GEX < 0 <=> dealer SHORT gamma
m["short_gamma"] = (m["dealer_net_gamma"] > 0).astype(float)
m["above_flip"] = (m["close"] > m["gamma_flip_level"]).astype(float)
m["rvp24"] = np.sqrt((m["r"] ** 2).rolling(24).mean())
m["rvp168"] = np.sqrt((m["r"] ** 2).rolling(168).mean())
m["ret168"] = np.log(m["close"] / m["close"].shift(168))
m["z"] = m["r"] / m["rvp24"].shift(1)
for k in (4, 24):
m[f"rvf{k}"] = np.sqrt((m["r"].shift(-1) ** 2).rolling(k).sum().shift(-(k - 1)) / k)
return m
def ols(y, cols):
y = np.asarray(y, float)
n = len(y)
X = np.column_stack([np.ones(n)] + [np.asarray(c, float) for c in cols])
b, *_ = np.linalg.lstsq(X, y, rcond=None)
res = y - X @ b
s2 = res @ res / (n - X.shape[1])
se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X)))
return b, b / se, 1.0 - np.var(res) / np.var(y)
def variance_ratio(r, k: int) -> float:
r = np.asarray(pd.Series(r).dropna(), float)
if len(r) < 5 * k:
return np.nan
x = pd.Series(r).rolling(k).sum().dropna().values
return float(np.var(x) / (k * np.var(r)))
# ================================================================== 1. IL DATO
def sezione_1_dato(snap: pd.DataFrame) -> dict:
hr("1. VALIDAZIONE DEL DATO — prima del segnale (fonte dismessa, mai certificata)")
out = {}
print(f"file: {SNAP_F.name} righe {len(snap)} {snap.ts.min()} -> {snap.ts.max()}")
# 1a. cadenza: il file NON e' omogeneo
d = snap[snap.asset == "BTC"]
gaps = d.ts.diff().dt.total_seconds().div(60).round(0).value_counts()
print(f"\n1a. CADENZA (BTC): distribuzione dei salti in minuti {dict(list(gaps.items())[:6])}")
pre = snap[snap.ts < HF_START]
post = snap[snap.ts >= HF_START]
print(f" -> {len(pre)//2} righe/asset a cadenza GIORNALIERA (12:00 UTC) fino al 2026-05-01,")
print(f" {len(post)//2} righe/asset a 15 min dopo. Il 'file da 4 mesi' e' 90 GIORNI utili.")
out["n_hf_per_asset"] = len(post) // 2
for a in ASSETS:
x = post[post.asset == a]
hrs = x.ts.dt.floor("h").nunique()
span = (x.ts.max().floor("h") - x.ts.min().floor("h")).total_seconds() / 3600 + 1
print(f" {a}: ore distinte {hrs} su {span:.0f} di span = copertura {100*hrs/span:.2f}%"
f" (buchi > 20 min: {int((x.ts.diff().dt.total_seconds()/60 > 20).sum())})")
# 1b. colonne MORTE — una colonna presente non e' un dato presente
print("\n1b. COLONNE MORTE (regola 8 del brief: contare cio' che VARIA, non le righe)")
for c in ["liquidation_long_risk", "liquidation_short_risk", "macro_days_to_event"]:
v = snap[c].dropna()
print(f" {c:26s} non-nulli {len(v):6d} ({100*len(v)/len(snap):5.1f}%) "
f"valori distinti {v.nunique()} {'*** COSTANTE = ZERO INFORMAZIONE ***' if v.nunique() <= 1 else ''}")
out["dead_cols"] = [c for c in ["liquidation_long_risk", "liquidation_short_risk"]
if snap[c].dropna().nunique() <= 1]
# 1c. dealer_net_gamma e gamma_flip_level: si muovono? sono plausibili?
print("\n1c. PLAUSIBILITA' delle due colonne di regime (finestra 15 min)")
for a in ASSETS:
x = post[post.asset == a]
g, f = x.dealer_net_gamma, x.gamma_flip_level
rel = x.spot / f - 1.0
print(f" {a} dealer_net_gamma: nan {100*g.isna().mean():4.1f}% frac>0 {(g>0).mean():.3f} "
f"consecutivi identici {(g.diff()==0).mean():.4f} -> si muove")
print(f" {a} gamma_flip_level: nan {100*f.isna().mean():4.1f}% "
f"consecutivi identici {(f.diff()==0).mean():.4f} "
f"(spot/flip-1) p05 {rel.quantile(.05):+.1%} med {rel.median():+.1%} p95 {rel.quantile(.95):+.1%}")
print(" -> BTC: il flip e' CONGELATO nel 61% dei passi e sta fino a +42% SOTTO lo spot al p95.")
print(" Un 'livello di flip' a 42% dallo spot non e' un livello di flip: e' degenere.")
# 1d. LE DUE COLONNE SI CONTRADDICONO
print("\n1d. CONTRADDIZIONE INTERNA fra le due colonne della STESSA fonte")
for a in ASSETS:
x = post[post.asset == a].dropna(subset=["dealer_net_gamma", "gamma_flip_level", "spot"])
c = np.corrcoef((x.dealer_net_gamma > 0).astype(float), (x.spot > x.gamma_flip_level).astype(float))[0, 1]
print(f" {a}: corr( dng>0 , spot>flip ) = {c:+.3f}")
print(" Nella convenzione classica dealer LONG gamma sta SOPRA il flip: le due colonne")
print(" dovrebbero correlare POSITIVAMENTE. Correlano NEGATIVAMENTE -> una delle due ha il")
print(" segno invertito. Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io.")
# 1e. quante EPISODI di regime ci sono davvero -> il numero che conta
print("\n1e. AMPIEZZA EFFETTIVA — 2.160 ore NON sono 2.160 osservazioni")
for a in ASSETS:
x = post[post.asset == a].dropna(subset=["dealer_net_gamma"])
lab = (x.dealer_net_gamma > 0).astype(int).values
runs = pd.Series(lab).groupby((pd.Series(lab).diff() != 0).cumsum()).size() * 0.25 # ore
hourly = pd.Series(lab).iloc[::4]
rho = float(hourly.autocorr(1))
neff = len(hourly) * (1 - rho) / (1 + rho)
print(f" {a}: {len(runs)} episodi; {int((runs>72).sum())} durano >72h e coprono il "
f"{100*runs[runs>72].sum()/runs.sum():.0f}% del tempo")
print(f" AR(1) orario del regime {rho:.4f} -> n_eff ~ {neff:.0f} su {len(hourly)} ore")
out[f"neff_{a}"] = NEFF[a] = neff
print(" -> l'errore standard onesto di ogni statistica condizionata al regime va moltiplicato")
print(f" per ~sqrt(2130/n_eff) = 6x (BTC) - 9x (ETH). E' IL numero della sezione 3.")
return out
# ================================================================== 2. SEGNO E RICOSTRUIBILITA'
def sezione_2_ricostruzione(snap: pd.DataFrame) -> dict:
hr("2. DA DOVE VIENE `dealer_net_gamma`, E POSSIAMO RIFARLO OGGI?")
out = {}
print("La domanda non e' accademica: bite e' dismesso, la serie finisce il 2026-07-30. Se il")
print("segnale non e' ricostruibile dalla catena che raccogliamo NOI, il lead e' morto alla nascita.")
arch = CHAIN_D / "bite_archive.parquet"
d = pd.read_parquet(arch, columns=["ts", "asset", "option_type", "strike", "gamma",
"open_interest", "source"])
d["ts"] = pd.to_datetime(d["ts"], utc=True).astype("datetime64[ns, UTC]")
d["tsf"] = d["ts"].dt.floor("15min")
per_snap = d.groupby(["source", "asset", "tsf"]).size().rename("n").reset_index()
print("\n2a. STRIKE PER SNAPSHOT nell'archivio ereditato (mediana):")
print(per_snap.groupby(["source", "asset"])["n"].median().to_string())
print(" -> `bite:live` ha ~16-18 strike: e' la manciata che serviva al suo motore VRP, NON")
print(" una catena. Solo `bite:research` (~280-320 strike) e' ricostruibile. E parte dal")
print(" 2026-06-09, non dal 05-01.")
# spot: l'archivio ha underlying_price 100% NaN -> lo prendiamo dallo snapshot (stessa fonte)
d = d[(d["source"] == "bite:research")].dropna(subset=["gamma", "open_interest"])
sp = (snap[["asset", "spot", "dealer_net_gamma", "gamma_flip_level"]]
.assign(tsf=snap["ts"].dt.floor("15min"))
.dropna(subset=["spot"]).drop_duplicates(["asset", "tsf"]))
d = d.merge(sp[["asset", "tsf", "spot"]], on=["asset", "tsf"], how="inner")
print(f"\n (l'archivio ha underlying_price 100% NaN -> spot preso dallo snapshot, stessa fonte)")
sgn = np.where(d["option_type"].values == "C", 1.0, -1.0)
S = d["spot"].values
G = d["gamma"].values * d["open_interest"].values
d["gex_std"] = G * S * S * 0.01 * sgn # GEX da manuale: dollar-gamma per 1%, call +, put -
d["gex_all"] = G * S * S * 0.01 # senza distinzione call/put
d["gex_nos"] = G * sgn # senza scala S^2
d["gex_S1"] = G * S * sgn # scala lineare in S
agg = (d.groupby(["asset", "tsf"])
.agg(**{c: (c, "sum") for c in ["gex_std", "gex_all", "gex_nos", "gex_S1"]},
n=("gamma", "size")).reset_index())
print("\n2b. LA RICOSTRUZIONE (catena near-full `bite:research` vs la colonna di bite)")
for a in ASSETS:
j = (agg[agg.asset == a].merge(sp[sp.asset == a][["tsf", "dealer_net_gamma"]], on="tsf")
.dropna(subset=["dealer_net_gamma"]))
print(f" {a}: n={len(j)} snapshot, ~{j.n.median():.0f} strike ciascuno")
for c in ["gex_std", "gex_all", "gex_nos", "gex_S1"]:
COUNT["ricostruzione"] += 1
rp = np.corrcoef(j[c], j.dealer_net_gamma)[0, 1]
rs = j[c].corr(j.dealer_net_gamma, method="spearman")
sa = float(np.mean((j[c] > 0) == (j.dealer_net_gamma > 0)))
sl, ic = np.polyfit(j[c], j.dealer_net_gamma, 1)
print(f" {c:8s} corr {rp:+.3f} rank {rs:+.3f} accordo-di-segno {sa:.3f}"
f" fit dng = {sl:+.3f}*GEX {ic:+.2e}")
best = j
out[f"corr_{a}"] = float(np.corrcoef(best.gex_std, best.dealer_net_gamma)[0, 1])
out[f"slope_{a}"] = float(np.polyfit(best.gex_std, best.dealer_net_gamma, 1)[0])
out[f"intc_{a}"] = float(np.polyfit(best.gex_std, best.dealer_net_gamma, 1)[1])
print("\n RISULTATO: il GEX da manuale riproduce la colonna di bite a corr -0.95 (BTC) /")
print(" -0.89 (ETH), con accordo-di-segno del 10%/8%. Cioe':")
print(" dealer_net_gamma ~ -0.7 * GEX_standard -> LA COLONNA E' IL GEX COL SEGNO INVERTITO.")
print(" `dealer_net_gamma > 0` significa dealer SHORT gamma, non long. Il nome mente.")
print(" Questo CHIUDE la contraddizione della sezione 1d: e' la colonna dng ad avere il segno")
print(" girato, il gamma_flip_level e' nella convenzione classica. Da qui in poi il codice usa")
print(" `short_gamma = dealer_net_gamma > 0`.")
print("\n2c. LA SOGLIA NON E' PORTABILE — e questo e' il punto che uccide il riuso diretto")
for a in ASSETS:
z = -out[f"intc_{a}"] / out[f"slope_{a}"]
print(f" {a}: dng=0 corrisponde a GEX = {z:+.3e}, non a GEX = 0.")
print(" Le due serie hanno zeri DIVERSI (universi diversi: il nostro collettore filtra OI>=100")
print(" e scadenze <=95g). Trasportare 'la soglia zero' da una all'altra e' un cambio di")
print(" definizione mascherato da continuita'. Ricalibrarla sui 90 giorni sarebbe selezione.")
print("\n2d. IL SEGNALE E' VIVO OGGI? (raccolta nostra, `pyg`, oraria)")
fs = sorted(glob.glob(str(CHAIN_D / "2026-08-*.parquet")))
if fs:
o = pd.concat([pd.read_parquet(f, columns=["ts", "asset", "option_type", "gamma",
"open_interest", "underlying_price",
"quote_status"]) for f in fs], ignore_index=True)
o["ts"] = pd.to_datetime(o["ts"], utc=True)
o = o.dropna(subset=["gamma", "open_interest", "underlying_price"])
s2 = np.where(o["option_type"].values == "C", 1.0, -1.0)
o["gex"] = (o["gamma"].values * o["open_interest"].values
* o["underlying_price"].values ** 2 * 0.01 * s2)
g = (o.groupby(["asset", o["ts"].dt.floor("h")])
.agg(gex=("gex", "sum"), n=("gamma", "size")).reset_index())
for a in ASSETS:
x = g[g.asset == a]
print(f" {a}: {len(x)} ore dal {x.ts.min():%Y-%m-%d} al {x.ts.max():%Y-%m-%d %H:%M}, "
f"~{x.n.median():.0f} strike/ora, quote ok {100*(o[o.asset==a].quote_status=='ok').mean():.1f}%")
print(f" GEX medio {x.gex.mean():+.3e} frac GEX>0 (dealer LONG gamma) {(x.gex>0).mean():.3f}"
f" ultimo {x.gex.iloc[-1]:+.3e}")
print(" -> RICOSTRUIBILE, oraria, viva, con MEGLIO del dato originale (segno giusto e")
print(" `quote_status` esplicito). Il lead e' inseguibile: il dato non e' il vincolo.")
print(" -> ma nota: ad agosto il GEX e' >0 nell'88-93% delle ore. Il regime 'short gamma'")
print(" compare a sprazzi: la variabile non varia quasi mai, e questo e' un problema")
print(" di potenza che il tempo aggiusta LENTAMENTE.")
else:
print(" (nessun file 2026-08-* nella raccolta: salto)")
return out
# ================================================================== 3. IL FATTO (A): la vol
def sezione_3_fatto_vol(M: dict, snap: pd.DataFrame, rec: dict) -> dict:
hr("3. GAMBA (A) DELL'IPOTESI — short gamma amplifica la vol realizzata?")
out = {}
rng = np.random.default_rng(SEED)
print("Misura il FATTO condizionale, non una strategia. Se un effetto e' vero si vede qui.")
print("\n3a. VOL REALIZZATA FORWARD, condizionata al regime (segno corretto in sezione 2)")
for a in ASSETS:
m = M[a]
for c in ["rvf4", "rvf24"]:
g = m.groupby("short_gamma")[c].mean() * ANN
COUNT["vol_condizionata"] += 1
print(f" {a} {c}: LONG gamma {g.get(0.0, np.nan):5.1f}% SHORT gamma {g.get(1.0, np.nan):5.1f}%"
f" spread {g.get(1.0,0)-g.get(0.0,0):+5.1f} pp")
print(" Direzione CONFORME al manuale: short gamma -> vol forward piu' alta. Prima gamba OK.")
print("\n3b. MA: il regime coincide quasi col MESE. Scomposizione (regola: un contributo si")
print(" scompone per periodo PRIMA di crederci — lezione SOL, 22/08)")
for a in ASSETS:
m = M[a].dropna(subset=["rvf24", "short_gamma"]).copy()
m["mo"] = m["dt"].dt.strftime("%Y-%m")
obs = (m.rvf24[m.short_gamma == 1].mean() - m.rvf24[m.short_gamma == 0].mean()) * ANN
print(f" {a} spread pieno {obs:+.1f} pp")
for mo, gg in m.groupby("mo"):
ns, nl = int(gg.short_gamma.sum()), int((1 - gg.short_gamma).sum())
sp = (gg.rvf24[gg.short_gamma == 1].mean() - gg.rvf24[gg.short_gamma == 0].mean()) * ANN
print(f" {mo}: spread {sp:+6.1f} pp ore SHORT {ns:4d} / LONG {nl:4d}"
f" {'<-- mese quasi interamente in UN regime' if min(ns,nl) < 100 else ''}")
out[f"spread_{a}"] = obs
print(" -> Maggio e Luglio sono mesi LONG-gamma, Giugno e' un mese SHORT-gamma. Il confronto")
print(" 'short vs long' e', in pratica, 'giugno vs maggio+luglio': UN confronto, non 2.136.")
print("\n3c. NULL a spostamento circolare (conserva l\'autocorrelazione di ENTRAMBE le serie,")
print(" rompe solo l\'allineamento) — e il suo LIMITE DI RISOLUZIONE")
for a in ASSETS:
m = M[a].dropna(subset=["rvf24", "short_gamma"])
lab, y = m.short_gamma.values, m.rvf24.values
n = len(y)
obs = (y[lab == 1].mean() - y[lab == 0].mean()) * ANN
null = np.empty(2000)
for i in range(2000):
L = np.roll(lab, rng.integers(24, n - 24))
null[i] = (y[L == 1].mean() - y[L == 0].mean()) * ANN
p = float((np.abs(null) >= abs(obs)).mean())
runs = pd.Series(lab).groupby((pd.Series(lab).diff() != 0).cumsum()).size()
long_runs = runs[runs > 24]
n_align = n / max(long_runs.median(), 1.0)
print(f" {a}: osservato {obs:+.1f} pp | null media {null.mean():+.2f} sd {null.std():.2f}"
f" | p empirico (2 code) {p:.4f}")
print(f" MA lo spostamento circolare di una serie con episodi lunghi ~{long_runs.median():.0f}h")
print(f" produce solo ~{n_align:.0f} allineamenti DISTINTI: la risoluzione del p non e\'")
print(f" 1/2000, e\' ~1/{n_align:.0f}. Un p 'zero' su {n_align:.0f} configurazioni non e\' 1e-4.")
out[f"p_{a}"] = p
print("\n3c-bis. LA PROVA CHE COSTA MENO DI TUTTE: si toglie GIUGNO (l\'unico mese short-gamma)")
for a in ASSETS:
m = M[a].dropna(subset=["rvf24", "short_gamma"]).copy()
m["mo"] = m["dt"].dt.strftime("%Y-%m")
for drop in (None, "2026-06"):
g = m if drop is None else m[m.mo != drop]
ns, nl = int(g.short_gamma.sum()), int((1 - g.short_gamma).sum())
sp = (g.rvf24[g.short_gamma == 1].mean() - g.rvf24[g.short_gamma == 0].mean()) * ANN
tag = "campione pieno" if drop is None else "senza giugno"
print(f" {a} {tag:16s}: spread {sp:+6.1f} pp ore SHORT {ns:4d} / LONG {nl:4d}")
print(" -> tolto UN mese su tre restano ~40-130 ore di regime short su 1.400: lo 'spread'")
print(" diventa il confronto fra due manciate di ore. Non e\' un campione, e\' un episodio.")
print("\n3d. NULL LOCATION-MATCHED per `gamma_flip_level`: livello di opzioni o media mobile?")
for a in ASSETS:
m = M[a]
g = m.groupby("above_flip")["rvf24"].mean() * ANN
sp_gamma = float(g.get(0.0, np.nan) - g.get(1.0, np.nan))
print(f" {a} GAMMA spot>flip: sotto {g.get(0.0, np.nan):.1f}% sopra {g.get(1.0, np.nan):.1f}%"
f" spread {sp_gamma:+.1f} pp")
best = None
for nn in (6, 12, 24, 48, 72, 120, 168, 240, 336, 504, 720):
COUNT["sma"] += 1
sma = m["close"].rolling(nn).mean()
ok = sma.notna()
agree = float((((m["close"] > m["gamma_flip_level"]) == (m["close"] > sma))[ok]).mean())
gg = m.groupby((m["close"] > sma).astype(float))["rvf24"].mean() * ANN
spread = float(gg.get(0.0, np.nan) - gg.get(1.0, np.nan))
if best is None or agree > best[1]:
best = (nn, agree, spread)
if best[2] > sp_gamma + 1.0:
verdetto = "il sosia da uno spread PIU GRANDE del livello di opzioni"
elif abs(best[2] - sp_gamma) <= 1.0:
verdetto = "il sosia da lo STESSO spread"
else:
verdetto = "il sosia da uno spread minore"
print(f" miglior sosia: spot>SMA({best[0]}h) -> accordo di regime {best[1]:.3f}, "
f"spread {best[2]:+.1f} pp ({verdetto})")
out[f"sma_agree_{a}"], out[f"sma_span_{a}"] = best[1], best[0]
out[f"sma_spread_{a}"], out[f"flip_spread_{a}"] = best[2], sp_gamma
print(" LETTURA, asset per asset (i due casi NON dicono la stessa cosa):")
print(" BTC: accordo 0.68, e la media mobile NUDA da' uno spread uguale o maggiore")
print(" (+8.8 contro +7.8 pp) -> il livello di opzioni non aggiunge nulla a una SMA.")
print(" ETH: accordo 0.96 -> `gamma_flip_level` E' una media mobile a 30 giorni con un")
print(" altro nome. Il suo spread e' piu' grande (+16.6 vs +10.1 pp), ma la")
print(" differenza vive tutta nel 4% di ore in cui i due regimi divergono: 4% di")
print(" 2.160 ore dentro un campione da 3 episodi non e' evidenza, e non e' testabile.")
print(" In nessuno dei due casi c'e' un contenuto 'opzioni' isolabile: e' la trappola 2 del")
print(" brief (TP01 travestito). `gamma_flip_level` non e' usabile come regime.")
print("\n3e. IL REGIME AGGIUNGE SOPRA CIO' CHE GIA' SAPPIAMO? (log rv fwd 24h ~ vol passata + trend)")
print(" t_eff = t / sqrt(24) corregge la sovrapposizione della finestra a 24h. NON corregge")
print(" l'ampiezza effettiva (sezione 1e: n_eff 24-53 su 2.130): un t_eff di +2.3 su 6-11")
print(" episodi indipendenti resta un t che conta episodi come se fossero ore.")
for a in ASSETS:
m = M[a]
d = m.dropna(subset=["rvf24", "rvp24", "rvp168", "ret168", "dvol", "short_gamma", "above_flip"])
d = d[d.rvf24 > 0]
y = np.log(d.rvf24)
base = [np.log(d.rvp24), np.log(d.rvp168), d.ret168]
d = d.assign(sma720=(d["close"] > d["close"].rolling(720).mean()).astype(float).fillna(0.0))
print(f" {a} (n={len(d)}) corr(short_gamma, DVOL) = "
f"{np.corrcoef(d.short_gamma, d.dvol)[0,1]:+.3f}")
for lab, extra in [("base: vol+trend", []),
(" + spot>SMA720", [d.sma720]),
(" + spot>flip", [d.above_flip]),
(" + short_gamma", [d.short_gamma]),
(" + DVOL", [np.log(d.dvol)]),
(" + DVOL + short_gamma", [np.log(d.dvol), d.short_gamma])]:
COUNT["regressioni"] += 1
b, t, r2 = ols(y, base + extra)
print(f" {lab:24s} R2 {r2:.3f} ultimo coef {b[-1]:+.3f} t {t[-1]:+6.2f}"
f" t_eff {t[-1]/np.sqrt(24):+5.2f}")
print(" -> BTC: `short_gamma` da solo spiega quanto DVOL da solo (R2 0.271 vs 0.272) e i due")
print(" insieme fanno 0.312 -> l'informazione e' in larga parte GIA' NEL DVOL, che il")
print(" progetto ha gia' provato come modulatore di TP01 il 26/06: era de-levering puro.")
print(" ETH: t_eff ~ +1.0 = rumore.")
return out
# ================================================================== 4. IL FATTO (B): MR vs trend
def sezione_4_fatto_mrtrend(M: dict) -> dict:
hr("4. GAMBA (B) DELL'IPOTESI — la gamba TRADEABILE: short gamma = trend, long gamma = MR?")
out = {}
print("Questa e' la gamba che diventerebbe un gate. La (A) e' solo il contorno.")
print("\n4a. VARIANCE RATIO per regime. VR<1 = mean-reversion, VR>1 = trend.")
print(" La versione 'z' standardizza ogni ritorno per la vol delle 24h precedenti: senza,")
print(" il clustering di volatilita' DENTRO un regime gonfia il VR e si legge come trend.")
for a in ASSETS:
m = M[a]
for lab, col in [("grezzo", "r"), ("z-std", "z")]:
L = {k: variance_ratio(m[col][m.short_gamma == 0], k) for k in (2, 4, 12, 24)}
S = {k: variance_ratio(m[col][m.short_gamma == 1], k) for k in (2, 4, 12, 24)}
COUNT["variance_ratio"] += 4
print(f" {a} {lab}: LONG " + " ".join(f"VR{k}={L[k]:.3f}" for k in (2, 4, 12, 24)))
print(f" {a} {lab}: SHORT " + " ".join(f"VR{k}={S[k]:.3f}" for k in (2, 4, 12, 24)))
if lab == "z-std":
out[f"vr24_long_{a}"], out[f"vr24_short_{a}"] = L[24], S[24]
print("\n LETTURA (versione z a 24h, l'unica scale-free):")
for a in ASSETS:
vl, vs = out[f"vr24_long_{a}"], out[f"vr24_short_{a}"]
if abs(vl - vs) < 0.05:
diag = "NESSUNA separazione (differenza < 0.05)"
elif vs > vl:
diag = "separazione NEL VERSO dell'ipotesi (short gamma piu' trendante)"
else:
diag = "separazione ROVESCIATA: e' il regime LONG gamma a trendare"
print(f" {a}: LONG {vl:.3f} vs SHORT {vs:.3f} -> {diag}")
print(" Due asset, due risposte diverse, e quella che separa lo fa al contrario. La gamba (B)")
print(" non e' 'debole': e' INCOERENTE. Con la gamba (A) confermata, questo e' il punto in cui")
print(" l'ipotesi si rompe — perche' e' la (B) che si traderebbe.")
print("\n4b. La forma tradeable: payoff di momentum per regime, sgn(ritorno passato L) x ritorno")
print(" futuro H. Sharpe ANNUALIZZATO LORDO (nessuna fee: se non regge lordo e' finita).")
Ls, Hs = (1, 4, 12, 24), (1, 4, 12, 24)
cells = []
for a in ASSETS:
m = M[a]
print(f" {a} " + " ".join(f"H={h:<2d}" for h in Hs) + " (L=long-gamma, S=short-gamma)")
for L in Ls:
past = np.log(m["close"] / m["close"].shift(L))
row = []
for H in Hs:
fwd = np.log(m["close"].shift(-H) / m["close"])
pay = np.sign(past) * fwd
for lab, mask in [("L", m.short_gamma == 0), ("S", m.short_gamma == 1)]:
v = pay[mask].dropna()
s = float(v.mean() / v.std() * np.sqrt(HOUR_YEAR / H)) if len(v) > 30 and v.std() > 0 else np.nan
COUNT["momentum"] += 1
cells.append((a, L, H, lab, s, v))
row.append(f"{lab}{s:+.2f}")
print(f" L={L:2d}h " + " ".join(row))
se = np.sqrt(HOUR_YEAR / len(m))
neff = NEFF.get(a, len(m))
print(f" [errore standard di UNA cella a H=1h: +/-{se:.2f} di Sharpe su {len(m)} ore.")
print(f" Con l'ampiezza effettiva della sezione 1e (n_eff={neff:.0f}) diventa"
f" +/-{se*np.sqrt(len(m)/neff):.1f}.")
print(" Ogni numero della tabella qui sopra e' dentro il proprio errore standard.]")
out["cells"] = cells
return out
# ================================================================== 5. IL GATE SUL LIBRO
def sezione_5_gate(snap: pd.DataFrame, fatti4: dict) -> dict:
hr("5. IL GATE SUL LIBRO — costruito e misurato, con la potenza dichiarata")
out = {}
print("La sezione 4 ha gia' refutato la gamba tradeable. Il gate si costruisce lo stesso, perche'")
print("un 'non funziona' misurato vale piu' di un 'non l'ho provato'.")
B = A.tp01_baseline_daily()
reg = {}
for a in ASSETS:
s = (snap[(snap.asset == a) & (snap.ts >= HF_START)]
.dropna(subset=["dealer_net_gamma"])[["ts", "dealer_net_gamma"]]
.set_index("ts").resample("1D").last())
# ultimo snapshot del giorno d -> usato per il giorno d+1: causale per costruzione
reg[a] = (s["dealer_net_gamma"] > 0).astype(float).shift(1)
R = pd.concat(reg, axis=1).mean(axis=1).dropna()
J = pd.concat({"B": B, "S": R}, axis=1, join="inner").dropna()
b = J["B"]
n = len(J)
se = np.sqrt(365.25 / n)
print(f"\n5a. FINESTRA: {J.index.min():%Y-%m-%d} -> {J.index.max():%Y-%m-%d}, {n} giorni.")
print(f" TP01 (baseline 50/50) su questa finestra: Sharpe {sh_d(b):+.2f} maxDD {dd_d(b):.2%}"
f" ritorno {(1+b).prod()-1:+.2%}")
print(f" ERRORE STANDARD DELLO SHARPE su {n} giorni: +/-{se:.2f}.")
print(" Cioe': su questa finestra TP01 e' indistinguibile da qualunque cosa fra -4 e 0. Il")
print(" libro non e' misurabile qui, e nessun gate lo sara'. E' il vincolo, non un dettaglio.")
print(f" (per giunta TP01 e' quasi FLAT: ritorno lordo {100*(1+b).prod()-100:+.2f}% in 3 mesi)")
out["n_days"], out["se_sharpe"] = n, se
print("\n5b. QUATTRO gate dichiarati (2 polarita' x 2 intensita'), + il null del de-levering")
variants = {
"classico SHORT->1.5x LONG->0.5x": (0.5, 1.5),
"classico mite SHORT->1.25 LONG->0.75": (0.75, 1.25),
"INVERSO SHORT->0.5x LONG->1.5x": (1.5, 0.5),
"risk-off in SHORT (1.0 / 0.5)": (1.0, 0.5),
}
rows = []
for name, (lo, hi) in variants.items():
x = b * (lo + (hi - lo) * J["S"])
COUNT["gate"] += 1
rows.append((name, sh_d(x), dd_d(x)))
print(f" {name:38s} Sharpe {sh_d(x):+.2f} maxDD {dd_d(x):.2%}")
print("\n NULL DEL DE-LEVERING (obbligatorio su ogni claim di DD — 5 occorrenze nel progetto):")
for k in (0.5, 0.6, 0.7, 0.8, 0.9, 1.0):
COUNT["delevering"] += 1
x = b * k
print(f" baseline x{k:.1f} Sharpe {sh_d(x):+.2f} maxDD {dd_d(x):.2%}")
best_dd = min(rows, key=lambda r: abs(r[2]))
print(f"\n -> il gate 'classico' porta il maxDD da {dd_d(b):.2%} a {rows[0][2]:.2%} con Sharpe"
f" {rows[0][1]:+.2f} (peggiore del baseline {sh_d(b):+.2f});")
print(f" il semplice `baseline x0.6` fa maxDD {dd_d(b*0.6):.2%} a Sharpe {sh_d(b*0.6):+.2f}.")
print(" NULL DE-LEVERING: **FALLITO**. Il gate e' meno leva con passaggi in piu'.")
out["null_delevering"] = "FALLITO"
print("\n5c. `marginal_vs_tp01` sullo stream INCREMENTALE del gate (cio' che il gate aggiunge)")
ov = (b * (0.5 + 1.0 * J["S"])) - b
try:
rep = A.marginal_vs_tp01(ov)
for k in ("marginal_verdict", "n_days", "n_hold_days", "corr_full", "cand_full_sharpe",
"beta_to_tp01", "reason"):
if k in rep:
print(f" {k:20s} {rep[k]}")
bl = rep.get("blends", {})
for w, v in bl.items():
print(f" blend {w}: full {v.get('full')} uplift_full {v.get('uplift_full')}"
f" uplift_hold {v.get('uplift_hold')}")
out["marginal"] = rep.get("marginal_verdict")
except Exception as e: # pragma: no cover
out["marginal"] = f"errore: {e}"
print(f" non girato: {e}")
print(" ATTENZIONE alla lettura: `n_hold_days` == `n_days` — i 90 giorni cadono INTERAMENTE")
print(" dentro l'hold-out di altlib, quindi 'full' e 'hold' sono LA STESSA FINESTRA e i loro")
print(" uplift coincidono (si vede sopra: -0.066 e -0.066). I gate multi-cut e il jackknife")
print(" di `marginal_vs_tp01` girano su una finestra sola.")
print(" Un ADDS qui non sarebbe un ADDS: sarebbe la definizione di selezione")
print(" sull'hold-out. Si riporta il verdetto, non lo si usa per promuovere.")
print("\n5d. DEFLATED SHARPE sulla cella migliore della griglia della sezione 4")
cells = [c for c in fatti4["cells"] if np.isfinite(c[4])]
best = max(cells, key=lambda c: c[4])
a, L, H, lab, s_best, v = best
allsr = [c[4] for c in cells]
try:
dsr, null_max = A.deflated_sharpe(s_best, allsr, pd.Series(v.values).dropna().values,
dpy=HOUR_YEAR / H)
reg = "SHORT" if lab == "S" else "LONG"
print(f" cella migliore: {a} L={L}h H={H}h regime={reg} gamma -> Sharpe LORDO {s_best:+.2f}"
f" (su {len(allsr)} trial dichiarati)")
print(f" deflated Sharpe = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0.95)")
print(f" massimo Sharpe ATTESO SOTTO IL NULLO con {len(allsr)} trial = {null_max:+.2f}")
if s_best < null_max:
print(f" -> la cella migliore ({s_best:+.2f}) sta SOTTO cio' che il puro rumore produce")
print(f" ({null_max:+.2f}) cercando {len(allsr)} volte. Non c'e' niente da deflazionare:")
print(" la griglia non ha prodotto nemmeno il massimo che il caso avrebbe prodotto.")
out["dsr"], out["dsr_nullmax"] = float(dsr), float(null_max)
except Exception as e: # pragma: no cover
out["dsr"] = f"errore: {e}"
print(f" non girato: {e}")
print("\n5e. CAUSALITA'")
print(" Per COSTRUZIONE: ogni riga usa `merge_asof(direction='backward')` sull'APERTURA della")
print(" barra con tolleranza 2h, quindi il regime della barra [t,t+1h) e' noto a t; il gate")
print(" giornaliero usa l'ultimo snapshot del giorno d-1 (`.shift(1)`).")
print(" `A.causality_ok` non e' applicabile: perturba il futuro dei prezzi certificati, ma il")
print(" regime qui non viene dai prezzi — viene da un file esterno che copre 90 giorni su 8")
print(" anni. Girarlo darebbe un PASS privo di potenza sul 97% del campione (stessa forma del")
print(" test a potenza zero corretto il 21/08). Si dichiara l'argomento strutturale, non un")
print(" numero finto.")
out["causality"] = "per costruzione (merge_asof backward + shift(1)); A.causality_ok non applicabile"
return out
# ================================================================== main
def main() -> None:
hr("r0822 DEALER-GAMMA — il regime di gamma dei dealer separa MR da trend su BTC/ETH?")
print("IPOTESI: (A) short gamma amplifica la vol; (B) short gamma fa TRENDARE il prezzo, long")
print("gamma lo fa MEAN-REVERTERE -> gate di regime sopra TP01/SKH01.")
print("ATTESA DICHIARATA PRIMA DI MISURARE: (A) plausibile ma confusa col livello di vol;")
print("(B) improbabile su 3 mesi e a rischio 'TP01 travestito'.")
snap = load_snap()
d1 = sezione_1_dato(snap)
d2 = sezione_2_ricostruzione(snap)
hf = snap[snap.ts >= HF_START]
M = {a: joined(a, hf) for a in ASSETS}
for a in ASSETS:
print(f"\n[merge] {a}: {len(M[a])} barre orarie con regime, "
f"{M[a].dt.min():%Y-%m-%d} -> {M[a].dt.max():%Y-%m-%d}")
d3 = sezione_3_fatto_vol(M, hf, d1)
d4 = sezione_4_fatto_mrtrend(M)
d5 = sezione_5_gate(snap, d4)
hr("VERDETTO")
tot = sum(COUNT.values())
print(f"GRIGLIA DICHIARATA: {tot} celle/statistiche valutate, contate al rialzo:")
for k, v in COUNT.items():
print(f" {k:18s} {v:4d}")
print(" Nessuna griglia e' stata ridotta per budget: il vincolo qui e' il DATO, non la macchina.")
print(f" Le {COUNT['momentum']} celle di momentum sono quelle passate al deflated Sharpe (5d).")
print()
print("SCARTATO. La gamba (A) e' confermata nella direzione ma non nella potenza; la gamba (B),")
print("che e' quella che si traderebbe, e' INCOERENTE fra i due asset e rovesciata dove separa.")
print()
print(" 1. Il dato regge meno di quanto dichiara: 15 min solo dal 2026-05-01 (90 giorni, non 4")
print(" mesi); 2 colonne su 16 COSTANTI; `gamma_flip_level` congelato al 61% su BTC.")
print(" 2. `dealer_net_gamma` e' il GEX COL SEGNO INVERTITO (corr -0.95 BTC / -0.89 ETH contro")
print(" la ricostruzione dalla catena). Chi lo usasse col nome che porta leggerebbe ogni")
print(" regime al contrario. -> Questo e' il risultato riusabile di tutto il filone.")
print(" 3. `gamma_flip_level` e' una media mobile: 96% di accordo con spot>SMA(720h) su ETH; su")
print(" BTC (accordo 0.68) la SMA nuda da' uno spread di vol MAGGIORE del livello stesso.")
print(" Niente contenuto di opzioni isolabile. Trappola 2 del brief.")
print(" 4. Ampiezza effettiva: 73 (BTC) / 59 (ETH) episodi di regime, ma SEI e OTTO di essi")
print(" coprono il 78-86% del tempo; n_eff 53 e 24 ore su 2.130. Il regime coincide col")
print(" MESE (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' un confronto")
print(" fra tre mesi, non fra 2.136 osservazioni. Tolto giugno cala del 46% (BTC) e del")
print(" 71% (ETH) e resta poggiato su 134 / 366 ore.")
print(" 5. Cio' che resta della gamba (A) e' quasi tutto DVOL (corr 0.66 su BTC), e il DVOL")
print(" come modulatore di TP01 e' gia' stato refutato il 26/06: era de-levering.")
print(" 6. Il gate misurato FALLISCE il null del de-levering (baseline x0.6 lo domina) su una")
print(" finestra dove SE(Sharpe) = +/-2.0 e TP01 e' praticamente flat.")
print(" 7. Deflated Sharpe 0.440 = FAIL, e nel modo piu' netto: la cella migliore delle 64")
print(" (+4.98 LORDO) sta SOTTO il massimo che 64 estrazioni di puro rumore producono")
print(" (+5.12). Non c'e' un candidato da deflazionare, c'e' una griglia sotto il rumore.")
print()
print("NON e' un LEAD: un lead richiede una soglia e una data che abbiano senso, e qui la soglia")
print("non e' nemmeno portabile fra la serie storica e quella che raccogliamo (zeri diversi,")
print("universi diversi). Ricalibrarla sui 90 giorni sarebbe la selezione che il progetto rifiuta.")
print()
print("COSA CONSERVARE (non e' una strategia, e' infrastruttura):")
print(" - il GEX si ricostruisce dalla NOSTRA catena, oraria, viva (518 ore dal 2026-08-01,")
print(" ~209-262 strike, quote ok ~100%), col segno GIUSTO e `quote_status` esplicito;")
print(" - se un giorno lo si riapre, si riapre su GEX ricostruito da noi, mai sulla colonna")
print(" ereditata, e non prima di avere ~30-40 EPISODI di regime (non 30-40 giorni):")
print(" al ritmo osservato (6-8 episodi / 90 giorni) sono ~2 anni, cioe' meta' 2028.")
print()
print("COSA MI SMENTIREBBE: una separazione del variance ratio z-standardizzato nello STESSO")
print("verso sui due asset (short gamma VR>1, long gamma VR<1) su GEX ricostruito da noi, con")
print("almeno 30 episodi di regime indipendenti e uno spread che sopravvive al controllo")
print("location-matched contro spot>SMA. Oggi ho l'opposto: BTC non separa, ETH separa al rovescio.")
print()
print(f"SCRIPT: scripts/research/r0822_dealer_gamma.py")
if __name__ == "__main__":
main()
+671
View File
@@ -0,0 +1,671 @@
"""r0822_flow_squeeze.py — filone FLOW-SQUEEZE (ondata 2026-08-22).
DOMANDA
Il posizionamento affollato (variazione rapida dell'open interest) ACCOPPIATO al rischio
di liquidazione produce un movimento prevedibile — squeeze o cascata — nelle ore successive?
E' l'unico angolo di "flusso" mai chiuso: il filone funding e' chiuso su 3 lati (carry CC01,
time-series, cross-sectional) ma sempre come LIVELLO del costo di carry, mai come PROXY DI
AFFOLLAMENTO accoppiato al rischio di liquidazione.
PERIMETRO (dichiarato: un altro filone dell'ondata lavora su dealer_net_gamma/gamma_flip_level,
che qui NON si toccano)
data/raw/cb_market_snapshots.parquet -> oi_delta_pct_4h, liquidation_long_risk,
liquidation_short_risk, funding_perp_annualized, funding_cross_annualized, iv_minus_rv
data/raw/hlfund_<sym>_1h.parquet -> funding orario Hyperliquid (BTC/ETH, ~3 anni)
data/raw/cb_chain/ -> open interest per strike (per la RICOSTRUIBILITA')
IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE
Attesa: NULLA di deployabile. (a) il dataset e' MORTO il 2026-07-30 (progetto esterno
dismesso) e ha ~3 mesi utili -> verdetto massimo LEAD; (b) il progetto ha gia' chiuso il
funding su 3 lati; (c) il sospetto principale e' che un eventuale effetto sull'estremo di
oi_delta sia "il prezzo e' appena crollato" travestito, cioe' momentum/mean-reversion di
prezzo gia' coperta. Cio' che NON mi aspettavo: che meta' dell'ipotesi (il rischio di
liquidazione) fosse **inesistente nel dato**.
TEST DI POTENZA — DICHIARATO PRIMA DI LEGGERE I RISULTATI (sezione 2)
Il campione utile e' ~90 giorni x 2 asset. La MDE (minimum detectable effect a 2 SE) si
calcola sul numero di finestre NON sovrapposte, non sulle righe orarie. Un effetto sotto la
MDE NON e' un fatto stabilito, comunque venga il p-value di un bootstrap su dati sovrapposti.
COME SI LEGGE IL RISULTATO
Le sezioni 1 e 5 (validazione e ricostruibilita') valgono piu' delle 2-4: se una colonna e'
costante non ha potenza, e se non sappiamo piu' produrla il segnale e' morto alla nascita.
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_flow_squeeze.py
"""
from __future__ import annotations
import bisect
import glob
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" / "alt"))
import altlib as A # noqa: E402
RAW = ROOT / "data" / "raw"
SNAP_FILE = RAW / "cb_market_snapshots.parquet"
ASSETS = ("BTC", "ETH")
# finestra utile: prima del 01/05 gli snapshot sono ~1/giorno (vedi sezione 1)
W0 = pd.Timestamp("2026-05-01", tz="UTC")
HOURS = (4, 12, 24)
# GRIGLIA DICHIARATA (conteggio al RIALZO, incluse le varianti scartate)
GRID_DECLARED = (
10 * 3 * 2 # 60 decili di oi_delta x 3 orizzonti x 2 lenti (demean per asset / per mese)
+ 4 * 2 # 8 2x2 affollamento (OI alto/basso x funding alto/basso) x 2 orizzonti
+ 4 * 2 # 8 specificazioni di regressione x 2 orizzonti
+ 10 * 2 # 20 decili di funding HL x 2 orizzonti
+ 3 * 3 * 2 # 18 griglia del segnale: soglia x orizzonte di holding x segno
+ 2 # 2 null del segnale (sempre-short, timing casuale)
+ 2 # 2 ricostruibilita' (2 asset)
) # = 118
def hdr(t: str) -> None:
print("\n" + "=" * 78 + f"\n{t}\n" + "=" * 78)
# ---------------------------------------------------------------------------
# dati
# ---------------------------------------------------------------------------
def load_snapshots() -> pd.DataFrame:
s = pd.read_parquet(SNAP_FILE)
# epoca ESPLICITA in ms: .view("int64") su tz-aware non-ns e' un look-ahead noto
s["ts_ms"] = s["ts"].astype("datetime64[ms, UTC]").astype("int64")
return s.sort_values("ts_ms").reset_index(drop=True)
def snap_num(snap: pd.DataFrame, asset: str, cols: list[str]) -> pd.DataFrame:
d = snap[snap.asset == asset][["ts_ms"] + cols].copy()
for c in cols:
d[c] = pd.to_numeric(d[c], errors="coerce")
return d.sort_values("ts_ms").reset_index(drop=True)
def merge_causal(df: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame:
"""Snapshot noto alla CHIUSURA della barra i (= timestamp[i] + 1h), backward, tolleranza 2h.
La posizione decisa a i e' poi tenuta durante i+1 (lo shift lo fa eval_weights)."""
left = pd.DataFrame({"ts_ms": (df["timestamp"].astype("int64") + 3_600_000).values})
m = pd.merge_asof(left, right, on="ts_ms", direction="backward",
tolerance=2 * 3600 * 1000)
return m.drop(columns=["ts_ms"])
def panel(snap: pd.DataFrame, cols: list[str], start=W0) -> pd.DataFrame:
out = []
for a in ASSETS:
df = A.get(a, "1h")
df = df[df.datetime >= start - pd.Timedelta(days=7)].reset_index(drop=True)
df = pd.concat([df, merge_causal(df, snap_num(snap, a, cols))], axis=1)
cc = df["close"].values.astype(float)
for H in HOURS:
f = np.full(len(cc), np.nan)
f[:-H] = cc[H:] / cc[:-H] - 1.0
df[f"f{H}"] = f
p4 = np.full(len(cc), np.nan)
p4[4:] = cc[4:] / cc[:-4] - 1.0
df["p4"] = p4
df["asset"] = a
out.append(df)
P = pd.concat(out, ignore_index=True)
return P[P.datetime >= start].reset_index(drop=True)
# ---------------------------------------------------------------------------
# 1. VALIDAZIONE DEL DATO
# ---------------------------------------------------------------------------
def sezione1(snap: pd.DataFrame) -> None:
hdr("1. VALIDAZIONE DEL DATO — le colonne vengono da un progetto esterno DISMESSO")
print(f"righe={len(snap)} ts {snap.ts.min()} -> {snap.ts.max()}")
cnt = snap[snap.asset == "BTC"].set_index("ts").resample("1D").size()
fasce = pd.cut(cnt, [-1, 0, 10, 50, 90, 400]).value_counts().sort_index()
print("\ngiorni per numero di snapshot/giorno (BTC):")
for k, v in fasce.items():
print(f" {str(k):>14s} : {v:3d} giorni")
pieni = cnt[cnt >= 90]
print(f" -> primo giorno a cadenza piena (>=90/g): {pieni.index.min().date()}"
f" ; giorni pieni = {len(pieni)}")
print(" ATTENZIONE: la copertura dichiarata parte dal 2026-03-26 ma 36 giorni hanno <=10")
print(" snapshot: la finestra USABILE a risoluzione oraria e' 2026-05-01 -> 2026-07-30.")
for a in ASSETS:
d = snap[snap.asset == a]
print(f"\n--- {a} (n={len(d)}) ---")
ok = d.fetch_ok.value_counts().to_dict()
print(f" fetch_ok: {ok} -> fallite {100*(1-d.fetch_ok.mean()):.1f}%")
for c in ("liquidation_long_risk", "liquidation_short_risk"):
vc = d[c].value_counts(dropna=False).to_dict()
nun = d[c].nunique()
flag = " <<< COSTANTE: ZERO POTENZA" if nun <= 1 else ""
print(f" {c:26s} categorie={nun} {vc}{flag}")
for c in ("oi_delta_pct_4h", "funding_perp_annualized", "funding_cross_annualized",
"iv_minus_rv", "macro_days_to_event"):
s = pd.to_numeric(d[c], errors="coerce")
mode_share = s.value_counts(normalize=True).iloc[0] if s.notna().any() else np.nan
print(f" {c:26s} nan={100*s.isna().mean():5.1f}% nuniq={s.nunique():5d} "
f"moda={s.mode().iloc[0] if s.notna().any() else float('nan'):+.4f} "
f"({100*mode_share:.1f}% delle righe) "
f"[{s.min():+.3f}, {s.median():+.3f}, {s.max():+.3f}]")
s = pd.to_numeric(d.oi_delta_pct_4h, errors="coerce").dropna()
same = (s.diff() == 0)
print(f" oi_delta_pct_4h: congelato {100*same.mean():.2f}% delle volte, "
f"autocorr(15m)={s.autocorr(1):+.3f} autocorr(4h)={s.autocorr(16):+.3f}")
print(" -> autocorr alta a 15m e ~0 a 4h = firma di una VERA finestra mobile a 4h "
"(non un valore inventato/congelato)")
print("\n>>> ESITO SEZIONE 1")
print(" (a) liquidation_long_risk e liquidation_short_risk valgono 'low' nel 100% delle")
print(" righe non-nulle, su 17.406 righe, 4 mesi, entrambi gli asset: UNA sola")
print(" categoria. META' DELL'IPOTESI E' UNTESTABILE PER COSTRUZIONE — non 'debole',")
print(" proprio senza varianza. E' il risultato piu' importante del filone.")
print(" (b) funding_perp_annualized e' ANCORATO al tasso base (0.01%/8h = 0.1095/anno)")
print(" nel ~49% (BTC) / ~60% (ETH) delle righe: come proxy di affollamento e' muto")
print(" per meta' del campione.")
print(" (c) oi_delta_pct_4h e' l'unica colonna del perimetro che si comporta da serie vera.")
# ---------------------------------------------------------------------------
# 2. IL FATTO
# ---------------------------------------------------------------------------
def bboot_p(x: np.ndarray, nb: int = 2000, block: int = 24, seed: int = 7) -> float:
x = np.asarray(x, float)
x = x[np.isfinite(x)]
n = len(x)
if n < block * 3:
return float("nan")
rng = np.random.default_rng(seed)
k = int(np.ceil(n / block))
starts = rng.integers(0, n - block, size=(nb, k))
means = np.array([np.concatenate([x[s:s + block] for s in starts[i]])[:n].mean()
for i in range(nb)])
return float((means >= 0).mean() if x.mean() < 0 else (means <= 0).mean())
def ols_bboot(d: pd.DataFrame, y: str, cols: list[str], nb: int = 500,
blk: int = 48, seed: int = 11):
X = np.column_stack([np.ones(len(d))] + [d[c].values for c in cols])
Y = d[y].values
b = np.linalg.lstsq(X, Y, rcond=None)[0]
rng = np.random.default_rng(seed)
n = len(Y)
k = int(np.ceil(n / blk))
bs = []
for _ in range(nb):
st = rng.integers(0, n - blk, size=k)
idx = np.concatenate([np.arange(s, s + blk) for s in st])[:n]
try:
bs.append(np.linalg.lstsq(X[idx], Y[idx], rcond=None)[0])
except np.linalg.LinAlgError:
pass
se = np.array(bs).std(0)
return b, se
def sezione2(snap: pd.DataFrame) -> pd.DataFrame:
hdr("2. IL FATTO — ritorno futuro condizionato all'OI (potenza DICHIARATA PRIMA)")
P = panel(snap, ["oi_delta_pct_4h", "funding_perp_annualized",
"funding_cross_annualized", "iv_minus_rv"])
d = P.dropna(subset=["oi_delta_pct_4h", "f24", "p4"]).copy()
print(f"campione: {len(d)} barre orarie ({d.datetime.min()} -> {d.datetime.max()})")
print("\n--- TEST DI POTENZA (dichiarato prima di leggere qualunque media) ---")
mde = {}
for H in HOURS:
n_ind = len(d) / H
sd = d[f"f{H}"].std()
mde[H] = 2 * sd / np.sqrt(n_ind / 10)
print(f" H={H:2d}h finestre NON sovrapposte={n_ind:6.0f} sd={100*sd:.2f}% "
f"n/decile={n_ind/10:5.0f} MDE(2 SE)={100*mde[H]:.3f}%")
print(" REGOLA PRE-REGISTRATA: un effetto di decile sotto la propria MDE NON e' un fatto")
print(" stabilito, qualunque p-value dia un bootstrap su osservazioni sovrapposte.")
for a in ASSETS:
m = d.asset == a
for H in HOURS:
d.loc[m, f"x{H}"] = d.loc[m, f"f{H}"] - d.loc[m, f"f{H}"].mean()
d["mese"] = d.datetime.dt.strftime("%Y-%m")
for H in HOURS:
d[f"y{H}"] = d[f"f{H}"] - d.groupby(["asset", "mese"])[f"f{H}"].transform("mean")
d["odec"] = d.groupby("asset").oi_delta_pct_4h.transform(
lambda s: pd.qcut(s, 10, labels=False))
drift = {a: {H: round(100 * d.loc[d.asset == a, f"f{H}"].mean(), 3) for H in HOURS}
for a in ASSETS}
print(f"\ndrift INCONDIZIONATO della finestra (%): {drift}")
print(" -> la finestra e' ribassista: ogni media condizionata va letta DEMEANATA,")
print(" altrimenti 'stare short' sembra un segnale.")
print("\n--- eccesso di ritorno futuro (%) per decile di oi_delta_pct_4h ---")
print(" lente 1 = demean per ASSET ; lente 2 = demean per (ASSET,MESE)")
g = d.groupby("odec").agg(n=("x4", "size"), oi=("oi_delta_pct_4h", "mean"),
x4=("x4", "mean"), x24=("x24", "mean"),
y4=("y4", "mean"), y24=("y24", "mean"),
av4=("f4", lambda s: s.abs().mean()),
av24=("f24", lambda s: s.abs().mean()))
for c in ("x4", "x24", "y4", "y24", "av4", "av24"):
g[c] *= 100
g["p_x4"] = [bboot_p(d[d.odec == k].x4.values) for k in g.index]
g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index]
print(g.round(3).to_string())
print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi")
print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75% a 4h, solo il decile 0")
print(" leggermente sopra): la 'cascata' prevedibile non c'e'.")
print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---")
z = lambda s: (s - s.mean()) / s.std() # noqa: E731
for c, n in [("oi_delta_pct_4h", "zoi"), ("p4", "zp4"),
("funding_perp_annualized", "zfund"), ("iv_minus_rv", "ziv")]:
d[n] = d.groupby("asset")[c].transform(z)
dr = d.dropna(subset=["zoi", "zp4", "zfund", "ziv"]).reset_index(drop=True)
print(f" corr(oi_delta, ritorno 4h passato) = {dr.oi_delta_pct_4h.corr(dr.p4):+.3f}"
f" (con 24h passato = {dr.oi_delta_pct_4h.corr(dr.f24.shift(24)):+.3f})")
for y in ("x4", "x24"):
for cols in (["zoi"], ["zp4"], ["zoi", "zp4"], ["zoi", "zp4", "zfund", "ziv"]):
b, se = ols_bboot(dr, y, cols)
txt = " ".join(f"{c}={100*b[i+1]:+.4f}%(t={b[i+1]/max(se[i+1],1e-12):+.2f})"
for i, c in enumerate(cols))
print(f" {y:4s} ~ {str(cols):36s} {txt}")
print(" -> la PENDENZA LINEARE di oi_delta e' nulla (|t|<1.3 in ogni specificazione).")
print(" Quel poco che c'e' vive SOLO nella coda estrema, e non e' momentum di prezzo")
print(" (corr con il ritorno passato ~0.02): e' un'altra cosa, ma e' una CODA.")
print("\n--- il decile 0 (OI che CROLLA) — l'unica cella non piatta ---")
for H in (4, 24):
obs = d[d.odec == 0][f"y{H}"].mean()
rng = np.random.default_rng(3)
nulls = []
for _ in range(2000):
sh = []
for a in ASSETS:
sub = d[d.asset == a]
k = rng.integers(1, len(sub) - 1)
sh.append(sub[f"y{H}"].values[np.roll((sub.odec == 0).values, k)])
nulls.append(np.concatenate(sh).mean())
nulls = np.array(nulls)
print(f" H={H:2d}h osservato={100*obs:+.3f}% MDE={100*mde[H]:.3f}% "
f"null circolare: mediana={100*np.median(nulls):+.3f}% "
f"p5={100*np.percentile(nulls,5):+.3f}% p={float((nulls<=obs).mean()):.3f}")
for a in ASSETS:
sub = d[(d.asset == a) & (d.odec == 0)]
cond = (d[d.asset == a].odec == 0).values
# AMPIEZZA EFFETTIVA: il numero di ORE non e' la taglia campionaria se la
# condizione e' fatta di pochi episodi lunghi (lezione del filone gemello
# dealer-gamma della stessa ondata: 73 "episodi" di cui 6-8 all'80% del tempo).
lens, cur = [], 0
for v in cond:
if v:
cur += 1
elif cur:
lens.append(cur); cur = 0
if cur:
lens.append(cur)
L = np.array(sorted(lens)[::-1])
print(f" {a}: ore in decile0={len(sub)} episodi distinti={len(L)} "
f"durata mediana={np.median(L):.1f}h max={L.max()}h "
f"quota ore negli 8 episodi piu' lunghi={L[:8].sum()/L.sum():.2f} "
f"y4={100*sub.y4.mean():+.3f}% y24={100*sub.y24.mean():+.3f}%")
print(" -> QUI l'ampiezza NON e' la patologia: 86-87 episodi corti (mediana 2h) e i")
print(" primi 8 coprono solo il 20-25% delle ore. Il vincolo binding e' la")
print(" LUNGHEZZA DEL CALENDARIO (90 giorni), non la concentrazione degli episodi.")
print("\n scomposizione PER MESE (demean mensile) — il test che il progetto impone")
print(" dopo il caso SOL ('un contributo positivo si scompone prima di crederci'):")
tab = d[d.odec == 0].groupby("mese")[["y4", "y24"]].agg(["size", "mean"])
for m_, r in tab.iterrows():
print(f" {m_}: n={int(r[('y4','size')]):4d} "
f"y4={100*r[('y4','mean')]:+.3f}% y24={100*r[('y24','mean')]:+.3f}%")
print("\n--- 2x2 AFFOLLAMENTO (l'ipotesi originale, col funding al posto della liquidazione) ---")
for c in ("oi_delta_pct_4h", "funding_perp_annualized"):
d[c + "_hi"] = d.groupby("asset")[c].transform(lambda s: s >= s.quantile(.8))
d[c + "_lo"] = d.groupby("asset")[c].transform(lambda s: s <= s.quantile(.2))
combos = [("OI su & funding ALTO (long affollati -> cascata?)",
d["oi_delta_pct_4h_hi"] & d["funding_perp_annualized_hi"]),
("OI su & funding BASSO (short affollati -> squeeze?)",
d["oi_delta_pct_4h_hi"] & d["funding_perp_annualized_lo"]),
("OI giu & funding ALTO ",
d["oi_delta_pct_4h_lo"] & d["funding_perp_annualized_hi"]),
("OI giu & funding BASSO",
d["oi_delta_pct_4h_lo"] & d["funding_perp_annualized_lo"])]
base = 100 * d.x4.abs().mean()
for nm, mask in combos:
sub = d[mask]
if len(sub) < 20:
print(f" {nm:52s} n={len(sub)} TROPPO PICCOLO")
continue
print(f" {nm:52s} n={len(sub):5d} x4={100*sub.x4.mean():+.3f}%(p={bboot_p(sub.x4.values):.3f})"
f" x24={100*sub.x24.mean():+.3f}%(p={bboot_p(sub.x24.values,block=48):.3f})"
f" |x4|={100*sub.x4.abs().mean():.3f}%")
print(f" |x4| di riferimento su tutto il campione = {base:.3f}%")
print(" -> 4 celle, 8 test: la sola sotto 0.05 (OI su & funding basso, x24) e' UNA cella su")
print(" 8 con ~8 finestre 24h indipendenti. Non e' un fatto.")
return d
# ---------------------------------------------------------------------------
# 3. FUNDING HL — l'unica gamba del perimetro con storia lunga
# ---------------------------------------------------------------------------
def sezione3() -> None:
hdr("3. FUNDING HYPERLIQUID BTC/ETH — 3 ANNI: l'unica gamba del perimetro con POTENZA")
res = []
for a in ASSETS:
f = RAW / f"hlfund_{a.lower()}_1h.parquet"
if not f.exists():
print(f" {f} assente")
return
h = pd.read_parquet(f)
h.index = pd.DatetimeIndex(h.index).tz_convert("UTC").floor("h")
h = h[~h.index.duplicated()]
df = A.get(a, "1h")
df = df.set_index(pd.DatetimeIndex(df.datetime))
j = df[["close"]].join(h[["funding", "premium"]], how="inner")
for H in (4, 24):
j[f"f{H}"] = j.close.shift(-H) / j.close - 1.0
j["asset"] = a
res.append(j)
J = pd.concat(res).dropna(subset=["funding", "f24"])
print(f"n={len(J)} barre orarie, {J.index.min().date()} -> {J.index.max().date()}")
print(" ATTENZIONE: il feed funding HL si ferma al 2026-06-22 (non e' aggiornato a oggi).")
base = J.funding.value_counts(normalize=True).iloc[0]
print(f" il funding e' ANCORATO al tasso base nel {100*base:.1f}% delle ore "
f"(valore {J.funding.mode().iloc[0]:.6f}/h) -> anche qui meta' campione e' muto.")
J["ye"] = J.index.year
J["fdec"] = J.groupby("asset").funding.transform(
lambda s: pd.qcut(s.rank(method="first"), 10, labels=False))
for H in (4, 24):
J[f"y{H}"] = J[f"f{H}"] - J.groupby(["asset", "ye"])[f"f{H}"].transform("mean")
g = J.groupby("fdec").agg(n=("y4", "size"), fund=("funding", "mean"),
y4=("y4", "mean"), y24=("y24", "mean"),
av4=("f4", lambda s: s.abs().mean()),
av24=("f24", lambda s: s.abs().mean()))
g["fund"] *= 100 * 24 * 365
for c in ("y4", "y24", "av4", "av24"):
g[c] *= 100
print("\n--- eccesso su drift ANNUALE (%) e |ret| futuro per decile di funding HL ---")
print(g.round(3).to_string())
print("\nestremi per ANNO (y24, %):")
t = J[J.fdec.isin([0, 9])].groupby(["fdec", "ye"]).y24.agg(["size", "mean"])
for k, r in t.iterrows():
print(f" dec={k[0]} anno={k[1]} n={int(r['size']):5d} y24={100*r['mean']:+.3f}%")
print("\n>>> ESITO SEZIONE 3: su 53.430 osservazioni e 3 anni — cioe' DOVE LA POTENZA C'E' —")
print(" il funding come proxy di affollamento non predice ne' la DIREZIONE (|eccesso|")
print(" <=0.35% a 24h, segno instabile fra anni) ne' la VOLATILITA' (|ret| futuro piatto")
print(" e non monotono). Il filone funding, gia' chiuso su 3 lati come livello di carry,")
print(" e' chiuso anche come proxy di affollamento.")
# ---------------------------------------------------------------------------
# 4. IL SEGNALE CAUSALE + I NULL + I GATE
# ---------------------------------------------------------------------------
def pct_expanding(x: np.ndarray, minobs: int = 200) -> np.ndarray:
"""Percentile ESPANDENTE causale: al bar i usa solo i valori <= i-1."""
out = np.full(len(x), np.nan)
seen: list[float] = []
for i, v in enumerate(x):
if np.isfinite(v):
if len(seen) >= minobs:
out[i] = bisect.bisect_left(seen, v) / len(seen)
bisect.insort(seen, v)
return out
class Signal:
"""SHORT (o LONG) quando il percentile causale di oi_delta_pct_4h e' sotto soglia,
tenuto H barre, vol-targeted 20%."""
def __init__(self, snap: pd.DataFrame):
self.SN = {a: snap_num(snap, a, ["oi_delta_pct_4h"]).dropna() for a in ASSETS}
def oi(self, df, asset):
return merge_causal(df, self.SN[asset])["oi_delta_pct_4h"].values.astype(float)
def make(self, thr: float, H: int, sign: int):
def fn(df, asset):
p = pct_expanding(self.oi(df, asset))
trig = np.where(np.isfinite(p), (p <= thr).astype(float), 0.0)
held = pd.Series(trig).rolling(H, min_periods=1).max().values
return A.vol_target(sign * held, df, target_vol=0.20, leverage_cap=2.0)
return fn
def book_net(target_by_asset) -> pd.Series:
nets = []
for a in ASSETS:
df = A.get(a, "1h")
m = (df.datetime >= W0).values
d2 = df[m].reset_index(drop=True)
ev = A.eval_weights(d2, np.asarray(target_by_asset(df, a))[m])
nets.append(pd.Series(ev["net"], index=ev["idx"]))
J = pd.concat(nets, axis=1, join="inner").fillna(0.0)
return 0.5 * J.iloc[:, 0] + 0.5 * J.iloc[:, 1]
def stats(net: pd.Series) -> dict:
if net.std() == 0:
return dict(sharpe=0.0, dd=0.0, ret=0.0, cagr=0.0)
eq = (1 + net).cumprod()
dd = float(((eq.cummax() - eq) / eq.cummax()).max())
yrs = (net.index[-1] - net.index[0]).total_seconds() / (365.25 * 86400)
return dict(sharpe=float(net.mean() / net.std() * np.sqrt(24 * 365.25)),
dd=dd, ret=float(eq.iloc[-1] - 1),
cagr=float(eq.iloc[-1] ** (1 / max(yrs, 1e-9)) - 1))
def sezione4(snap: pd.DataFrame) -> None:
hdr("4. IL SEGNALE CAUSALE — griglia 18 celle, i null, i gate")
S = Signal(snap)
rows = []
for thr in (0.10, 0.20, 0.30):
for H in (4, 12, 24):
for sign in (-1, +1):
st = stats(book_net(S.make(thr, H, sign)))
rows.append(dict(thr=thr, H=H, sign=sign, **st))
print(f" thr={thr:.2f} H={H:2d} sign={sign:+d} "
f"Sharpe(90g)={st['sharpe']:+.2f} maxDD={100*st['dd']:5.2f}% "
f"ret={100*st['ret']:+6.2f}%")
R = pd.DataFrame(rows)
best = R.loc[R.sharpe.idxmax()]
print(f"\n miglior cella: thr={best.thr} H={int(best.H)} sign={int(best.sign):+d} "
f"-> Sharpe {best.sharpe:.2f}")
print(f" SE(Sharpe) su 90 giorni = sqrt(365/90) = {np.sqrt(365/90):.2f} -> ogni Sharpe")
print(" sotto ~4 e' indistinguibile da zero su questa finestra. Non esiste hold-out:")
print(" la cella e' scelta sull'UNICO campione (select_cell_insample non applicabile).")
thr, H, sign = float(best.thr), int(best.H), int(best.sign)
tgt = S.make(thr, H, sign)
net = book_net(tgt)
st = stats(net)
print("\n--- NULL 1: 'sei solo short in una finestra ribassista?' ---")
always = lambda df, a: A.vol_target( # noqa: E731
sign * np.ones(len(df)), df, target_vol=0.20, leverage_cap=2.0)
st_a = stats(book_net(always))
tim = np.mean([np.mean(np.asarray(tgt(A.get(a, "1h"), a))[
(A.get(a, "1h").datetime >= W0).values] != 0) for a in ASSETS])
print(f" candidato Sharpe={st['sharpe']:+.2f} (tempo a mercato {tim:.3f}) "
f"sempre-esposto Sharpe={st_a['sharpe']:+.2f}")
print(" -> il candidato NON e' la statica travestita: il timing porta qualcosa.")
print("\n--- NULL 2: timing CASUALE a pari esposizione (shift circolare, 300 estrazioni) ---")
print(" e' il null LOCATION-MATCHED nella dimensione del tempo: stessa esposizione, stessa")
print(" struttura di episodi, stessa finestra — cambia SOLO QUANDO. Un null piu' ingenuo")
print(" (posizione casuale iid) direbbe di si' a qualunque cosa in una finestra con drift.")
base_trig = {}
for a in ASSETS:
df = A.get(a, "1h")
base_trig[a] = (np.asarray(tgt(df, a)), (df.datetime >= W0).values)
rng = np.random.default_rng(21)
nulls = []
for _ in range(300):
shifted = {}
for a in ASSETS:
v, m = base_trig[a]
v2 = v.copy()
idx = np.where(m)[0]
v2[idx] = np.roll(v2[idx], rng.integers(1, len(idx) - 1))
shifted[a] = v2
nulls.append(stats(book_net(lambda df, a: shifted[a]))["sharpe"])
nulls = np.array(nulls)
pctl = float((nulls < st["sharpe"]).mean())
print(f" null: mediana={np.median(nulls):+.2f} p90={np.percentile(nulls,90):+.2f} "
f"p99={np.percentile(nulls,99):+.2f} -> candidato al {100*pctl:.1f}o pctl")
print(f" ma la cella e' il MASSIMO di 18: P(almeno una cella oltre quel pctl per caso) "
f"= 1-{pctl:.3f}^18 = {1-pctl**18:.2f} -> il null non e' superato dopo il conto dei trial.")
print("\n--- GATE ---")
print(f" causality_ok : {A.causality_ok(tgt, tf='1h')}")
cd = A.candidate_daily(tgt, tf="1h")
cd = cd[cd.index >= W0]
sr = float(cd.mean() / cd.std() * np.sqrt(365.25))
rep = A.marginal_vs_tp01(cd)
print(f" candidate_daily : n={len(cd)} giorni, Sharpe={sr:.2f}")
n_hold = int((cd.index >= A.HOLDOUT).sum())
print(f" marginal_vs_tp01 : verdetto={rep.get('marginal_verdict')} "
f"beta_to_tp01={rep.get('beta_to_tp01')} has_insample_edge={rep.get('has_insample_edge')} "
f"is_hedge={rep.get('is_hedge')} robust_oos={rep.get('robust_oos')} "
f"beats_noise_null={rep.get('beats_noise_null')} null_pctl_full={rep.get('null_pctl_full')}")
print(f" ATTENZIONE: giorni totali={len(cd)}, giorni dopo HOLDOUT({A.HOLDOUT.date()})"
f"={n_hold} -> COINCIDONO")
print(f" (cand_full_sharpe={rep.get('cand_full_sharpe')} == "
f"cand_hold_sharpe={rep.get('cand_hold_sharpe')}). Il candidato vive INTERAMENTE")
print(" dentro l'hold-out del progetto: FULL e HOLD-OUT sono la STESSA finestra e la")
print(" meta' out-of-sample del gate e' strutturalmente INERTE (robust_oos=False non")
print(" giudica il candidato, dice che il multi-cut non esiste). Da leggere come")
print(" 'girato ma NON informativo sull'OOS', non come un NEUTRAL guadagnato sul campo.")
all_sr = list(R.sharpe.values)
d18 = A.deflated_sharpe(sr, all_sr, cd)
d_all = A.deflated_sharpe(sr, all_sr * 7, cd)
print(f" deflated_sharpe(18) : DSR={d18[0]:.3f} (Sharpe atteso del massimo sotto il null "
f"= {d18[1]:.2f}) -> {'PASS' if d18[0]>=0.95 else 'FAIL'}")
print(f" deflated_sharpe({GRID_DECLARED}) : DSR={d_all[0]:.3f} (null max {d_all[1]:.2f}) "
f"-> {'PASS' if d_all[0]>=0.95 else 'FAIL'}")
print(" con 114 giorni il MASSIMO atteso PER CASO su 18 celle e' Sharpe ~5.4:")
print(" il candidato (3.6) sta SOTTO il proprio null. Non serve altro.")
imp = A.implausible_sharpe(cd)
print(f" implausible_sharpe : {imp['implausible']} {imp['reasons']}")
print(" null de-levering : NON GIRATO — il candidato non fa alcun claim di riduzione")
print(" di drawdown (e' uno stream di ritorno, non un overlay).")
print(" anchor_luck_band : NON GIRATO — il segnale non e' ancorato a un'ora di")
print(" ribilanciamento (gira su ogni barra oraria).")
for a in ASSETS:
df = A.get(a, "1h")
m = (df.datetime >= W0).values
sc = A.eval_weights_smallcap(df[m].reset_index(drop=True),
np.asarray(tgt(df, a))[m], capital=600)
print(f" smallcap $600 {a} : haircut Sharpe={sc['sharpe_haircut']:+.3f} "
f"(reale {sc['realistic']['sharpe']:.2f} vs modellato {sc['modeled']['sharpe']:.2f}, "
f"{sc['n_executed_trades']} trade eseguiti)")
print(" -> l'ESEGUIBILITA' non e' il vincolo (2 gambe BTC/ETH perp, haircut ~0.02):")
print(" come per SOL, il candidato muore sull'evidenza, non sulla taglia del conto.")
print("\n--- CONCENTRAZIONE (dove sta davvero il P&L) ---")
mm = cd.groupby(cd.index.strftime("%Y-%m")).agg(n="size", ret=lambda s: 100 * ((1 + s).prod() - 1))
print(mm.round(2).to_string())
tot = 100 * ((1 + cd).prod() - 1)
top3 = 100 * cd.sort_values().tail(3).sum()
print(f" somma dei 3 giorni migliori = {top3:.2f}% su un totale di {tot:.2f}% "
f"({100*top3/max(tot,1e-9):.0f}% del P&L in 3 giorni su 114)")
# ---------------------------------------------------------------------------
# 5. RICOSTRUIBILITA'
# ---------------------------------------------------------------------------
def sezione5(snap: pd.DataFrame) -> None:
hdr("5. RICOSTRUIBILITA' — il dato e' MORTO il 2026-07-30: sapremmo rifarlo oggi?")
cols = ["asset", "instrument_name", "open_interest", "ts"]
parts = []
arch = RAW / "cb_chain" / "bite_archive.parquet"
if arch.exists():
parts.append(pd.read_parquet(arch, columns=cols))
for f in sorted(glob.glob(str(RAW / "cb_chain" / "2026-*.parquet"))):
parts.append(pd.read_parquet(f, columns=cols))
C = pd.concat(parts, ignore_index=True)
del parts
C["ts"] = pd.to_datetime(C.ts, utc=True).dt.floor("h")
C = C.drop_duplicates(["ts", "instrument_name"])
print(f"catena: {len(C)} righe {C.ts.min()} -> {C.ts.max()}")
sn = snap.copy()
sn["h"] = pd.to_datetime(sn.ts, utc=True).dt.floor("h")
for asset in ASSETS:
s = C[C.asset == asset]
piv = s.pivot_table(index="ts", columns="instrument_name",
values="open_interest", aggfunc="last")
piv = piv.reindex(pd.date_range(piv.index.min(), piv.index.max(), freq="h"))
V = piv.values
dl = np.full(len(piv), np.nan)
for i in range(4, len(piv)):
m = np.isfinite(V[i]) & np.isfinite(V[i - 4])
if m.sum() < 30:
continue
b = V[i - 4][m].sum()
if b > 0:
dl[i] = 100 * (V[i][m].sum() / b - 1)
chain = pd.Series(dl, index=piv.index, name="chain")
ref = pd.to_numeric(sn[sn.asset == asset].groupby("h").oi_delta_pct_4h.last(),
errors="coerce").rename("snap")
j = pd.concat([ref, chain], axis=1).dropna()
print(f" {asset}: ore sovrapposte={len(j)} "
f"corr Pearson={j.snap.corr(j.chain):+.3f} "
f"Spearman={j.snap.corr(j.chain, method='spearman'):+.3f} "
f"(sd snap={j.snap.std():.3f} vs sd catena={j.chain.std():.3f})")
del piv, V
print("\n Nota: l'OI della catena e' l'OI delle OPZIONI a strumenti APPAIATI (stessi")
print(" strumenti a t e t-4h, cosi' le scadenze non creano salti). La correlazione con")
print(" oi_delta_pct_4h e' ~0 in rango: NON e' la stessa grandezza (lo snapshot misurava")
print(" l'OI dei PERPETUAL/futures via il progetto dismesso).")
print("\n>>> ESITO SEZIONE 5 — colonna per colonna, cosa sappiamo produrre OGGI:")
print(" oi_delta_pct_4h : NO. La catena da' OI di OPZIONI (rango ~0.05 con la")
print(" colonna). L'OI perp Deribit e' leggibile dall'API")
print(" pubblica ma NON lo raccogliamo: la storia parte da")
print(" zero il giorno che si accende un collettore.")
print(" liquidation_long/short : NO, e non importa: la colonna e' costante 'low'.")
print(" Nessuna fonte nostra la produce, e non c'e' niente")
print(" da produrre.")
print(" funding_perp/cross : SI ma degradato — funding HL c'e' (fermo al 22/06/2026),")
print(" funding Deribit non lo salviamo.")
print(" iv_minus_rv : SI, ricostruibile da dvol_*.parquet + barre certificate.")
# ---------------------------------------------------------------------------
def main() -> None:
print(__doc__)
print(f"GRIGLIA DICHIARATA (conteggio al rialzo) = {GRID_DECLARED} celle/trial")
snap = load_snapshots()
sezione1(snap)
sezione2(snap)
sezione3()
sezione4(snap)
sezione5(snap)
hdr("6. VERDETTO")
print("""SCARTATO.
1. META' DELL'IPOTESI NON ESISTE NEL DATO: liquidation_long_risk e liquidation_short_risk
valgono 'low' nel 100% delle 17.229 righe non-nulle. Una variabile con UNA categoria non
ha potenza: l'accoppiamento affollamento x rischio-di-liquidazione e' untestabile, non
debole. (Ed e' esattamente la parte che rendeva l'angolo NUOVO rispetto al filone funding.)
2. IL FATTO NON REGGE LA PROPRIA SOGLIA DI POTENZA, dichiarata prima di guardare: l'unico
effetto e' il decile piu' basso di oi_delta (-0.40% a 24h) e la MDE a 24h e' 1.06%.
La pendenza lineare e' nulla (|t|<1.3 in 4 specificazioni su 4); il |ritorno| futuro e'
quasi piatto fra i decili (0.59-0.75% a 4h, solo il decile 0 leggermente sopra) -> la
'cascata' non c'e'; e il segno e' ASSENTE nel primo dei tre mesi (maggio y24=+0.02%,
giugno -0.64%, luglio -0.74%): 2 mesi su 3 non sono un plateau, sono due mesi.
3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0,
deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di
Sharpe 5.4; implausible_sharpe=True; e il P&L e' per il 71% di giugno e per il 60%
concentrato in 3 giorni su 114. marginal_vs_tp01 gira e dice NEUTRAL, ma va dichiarato
NON INFORMATIVO SULL'OOS: il candidato sta interamente dopo HOLDOUT(2025-01-01), quindi
full e hold-out sono la stessa finestra (Sharpe identico) e il multi-cut non esiste.
4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati
che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e
la sorgente e' stata dismessa il 2026-07-30.
Sottoprodotto con potenza vera (53.430 ore, 3 anni): il funding HL non predice ne' direzione
ne' volatilita' -> il filone funding, gia' chiuso su 3 lati come livello di carry, si puo'
dichiarare chiuso anche come PROXY DI AFFOLLAMENTO.""")
if __name__ == "__main__":
main()
+564
View File
@@ -0,0 +1,564 @@
#!/usr/bin/env python
"""r0822_growth_policy.py — LA POLITICA DI LEVA, giudicata sul TEMPO invece che sullo Sharpe.
LA DOMANDA. `target_vol=20%` per TP01 e' canonico dal 2026-06-19 e non e' mai stato messo in
discussione **rispetto a un obiettivo di crescita**. E' stato scelto (e ri-confermato: peso 75/25
tre volte, TP01xDVOL, tranching) sempre con una lente di **Sharpe** o di **drawdown**. Ma
l'operatore non massimizza lo Sharpe: vuole ~$258k netti il prima possibile. Sono due problemi
diversi — lo Sharpe e' invariante alla leva, il TEMPO no.
Quindi: **quale leva k minimizza il tempo mediano al bersaglio, a parita' di rischio accettato?**
E cambia se il bersaglio e' $50k invece di $258k?
COSA C'E' QUI
(0) Replica di controllo: il book live, il fattore d'ancora ×0.89 misurato, e i muri pubblicati.
(1) LA CURVA DI CRESCITA g(k) — dove sta il massimo (Kelly empirico) e da che parte sta il libro.
(2) SWEEP PRINCIPALE — per ogni k: tempo mediano al bersaglio (CONDIZIONATO, stampato accanto
alla sua probabilita'), P(entro 10/15/20a), P(rovina), P(DD>30%). Versamenti €0 e €500/mese,
bersagli $50k e il muro NETTO. Tutto **al netto del fisco d'accumulo** (33% + carry 4a + 0.2%).
(3) FRONTIERA ISO-ROVINA — la domanda del brief nella sua forma esatta.
(4) IL GATE CHE CONTA — il massimo di (1) e' calcolato su un campione che non contiene un solo
giorno peggiore di -3.9%. Stress esplicito: giorno di crash fuori campione + liquidazione
Deribit. Dove va il massimo quando il rischio che il dataset NON contiene entra nel conto.
(5) ESEGUIBILITA' — `config/live.json` oggi tiene il nozionale lordo <= 1.00x l'equity, e il
knob `target_vol` di TP01 NON scala linearmente (leverage cap 2x). Quanto di k e' teorico.
ONESTA' (regole del progetto applicate qui):
· un non-arrivo e' **+infinito**, mai -1 (l'errore del 07/08: con -1 la mediana MIGLIORA quanto
peggio va la colonna);
· una mediana condizionata si stampa **sempre** accanto alla sua probabilita';
· un Monte Carlo ha una **risoluzione**: la riga di testa e' rifatta con un secondo seme e la
banda e' dichiarata;
· la rovina non e' l'azzeramento aritmetico (con ritorni moltiplicativi il capitale non tocca
zero finche' k < 1/|peggior giorno| = 25.4): e' definita e dichiarata sotto.
nice -n 19 timeout 900 uv run python scripts/research/r0822_growth_policy.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import r0725_capcurve as CC # noqa: E402 macchineria validata (book_series, _boot_paths, survival)
import r0726_deposits as DP # noqa: E402 deluck_returns() con il ×0.89 MISURATO
import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE
from r0807_piano_netto import passo_fiscale # noqa: E402 il passo fiscale gia' validato
# ------------------------------------------------------------------ parametri dichiarati
KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 2.50, 3.50, 5.00, 7.00,
10.00, 14.00, 18.00, 22.00) # 14 celle: la griglia stretta (<=8x) NON conteneva il
# massimo -> allargata finche' il massimo e' INTERNO
KS_STRESS = (1.00, 1.50, 2.00, 3.50, 5.00, 10.00, 18.00) # sottoinsieme per la (4)
DEPOSITI = (0, 500) # €/mese: nessuno, e il piano dichiarato
BERSAGLI = {"$50k": 50_000.0, "muro netto": 258_338.0} # r0807_piano_netto (fisco coerente)
ANNI = 25 # orizzonte di simulazione: 25 come r0726/r0807 (20 troncherebbe
# la coda della mediana condizionata). Le P si leggono a 10/15/20a.
N_PATHS = 2_500
BLOCK = 20
CHUNK = 500 # RAM: la VPS ha ~3.5 GB liberi e ci girano altri agenti
SEED = 20260822
SEED_BIS = 8221 # secondo seme, solo per dichiarare la risoluzione MC
START = 600.0
ALIQUOTA = CC.TAX_RATE # 0.33
PATRIM = TX.PATRIMONIALE # 0.002
RUIN_MULT = 0.20 # rovina = il moltiplicatore del PATH perde l'80%
DD_SOGLIA = 0.30
WIPE = 0.90 # perdita giornaliera >= 90% dell'equity = liquidazione (margine+fee)
MURO_PUBBL_LORDO = 272_061.0 # r0726_capwall_refresh (convenzione pubblicata)
def sezione(t: str) -> None:
print("\n" + "=" * 104)
print(f" {t}")
print("=" * 104)
# ===========================================================================
# il motore: una passata, tutte le leve insieme (confronto APPAIATO sugli stessi path)
# ===========================================================================
def simula(r: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float,
bersagli: dict[str, float], aliquota: float, patrimoniale: float,
n_paths: int = N_PATHS, seed: int = SEED, start: float = START,
crash_pct: float = 0.0, crash_p_anno: float = 0.0) -> dict:
"""Accumulo a leva `k` per ogni k, sugli STESSI path (differenze appaiate).
Stato per (path, k). Traccia in parallelo un GEMELLO SENZA VERSAMENTI: il drawdown e la
rovina si misurano su quello, cosi' sono confrontabili fra colonne con versamenti diversi
(un bonifico mensile maschera un drawdown senza ridurre il capitale distrutto).
`crash_pct`/`crash_p_anno`: giorno di coda FUORI CAMPIONE (sezione 4). Il block bootstrap non
puo' generare un giorno peggiore del peggiore osservato: se lo si vuole, va messo a mano.
Liquidazione: se la perdita del giorno a leva k supera `WIPE` dell'equity, il conto e' chiuso
dall'exchange -> capitale a 0 (i versamenti successivi ripartono da li').
"""
kv = np.asarray(ks, float)[None, :]
nk = len(ks)
dep = dep_eur * CC.EURUSD
n_days = int(anni * 365)
rng = np.random.default_rng(seed)
nomi = list(bersagli)
soglie = np.array([bersagli[n] for n in nomi], float)
hit = {n: [] for n in nomi}
fin, mult_fin, ruin, ddmax, versato_tot = [], [], [], [], 0.0
for c0 in range(0, n_paths, CHUNK):
m = min(CHUNK, n_paths - c0)
paths = CC._boot_paths(r, m, n_days, BLOCK, rng)
if crash_pct > 0 and crash_p_anno > 0:
# un giorno di crash con probabilita' `crash_p_anno`/anno, uniforme nel tempo
p_giorno = crash_p_anno / 365.0
mask = rng.random((m, n_days)) < p_giorno
paths = np.where(mask, -abs(crash_pct), paths)
cap = np.full((m, nk), start, float)
mult = np.ones((m, nk), float) # gemello senza versamenti
hwm = np.ones((m, nk), float)
dd = np.zeros((m, nk), float)
rovinato = np.zeros((m, nk), bool)
colpito = {n: np.full((m, nk), np.inf) for n in nomi}
carry = np.zeros((m * nk, TX.CARRY_ANNI))
anno_start = cap.copy()
flusso = np.zeros((m, nk))
versato = start
for t in range(n_days):
rt = paths[:, t][:, None] * kv # (m, nk)
liq = rt <= -WIPE
g = np.where(liq, -1.0, rt)
cap *= (1.0 + g)
cap = np.maximum(cap, 0.0)
mult *= (1.0 + g)
mult = np.maximum(mult, 0.0)
hwm = np.maximum(hwm, mult)
dd = np.maximum(dd, 1.0 - mult / hwm)
rovinato |= mult <= RUIN_MULT
if dep > 0 and t % 30 == 0 and t > 0:
cap += dep
flusso += dep
versato += dep
for j, n in enumerate(nomi):
nuovi = np.isinf(colpito[n]) & (cap >= soglie[j])
colpito[n][nuovi] = t
if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0):
passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(),
carry, aliquota, patrimoniale)
anno_start = cap.copy()
flusso[:] = 0.0
for n in nomi:
hit[n].append(colpito[n])
fin.append(cap)
mult_fin.append(mult)
ruin.append(rovinato)
ddmax.append(dd)
versato_tot = versato
out = dict(versato=versato_tot, anni=anni, n_paths=n_paths, ks=ks)
out["cap"] = np.concatenate(fin, axis=0)
out["mult"] = np.concatenate(mult_fin, axis=0)
out["ruin"] = np.concatenate(ruin, axis=0)
out["dd"] = np.concatenate(ddmax, axis=0)
out["hit"] = {n: np.concatenate(hit[n], axis=0) for n in nomi}
return out
def anni_cond(hit_col: np.ndarray) -> tuple[float, float, float, float]:
"""(anni mediani CONDIZIONATI all'arrivo, P<=10a, P<=15a, P<=20a). Non-arrivo = +inf."""
arr = hit_col[np.isfinite(hit_col)]
med = float(np.median(arr)) / 365.0 if len(arr) >= 50 else float("nan")
p = lambda y: float((hit_col <= y * 365).mean()) # noqa: E731
return med, p(10), p(15), p(20)
def fmt_anni(a: float) -> str:
return f"{a:.1f}a" if np.isfinite(a) else "mai"
# ===========================================================================
def main() -> None:
print("=" * 104)
print(" r0822 — GROWTH POLICY: quale leva minimizza il TEMPO al bersaglio, a pari rovina accettata")
print("=" * 104)
# ------------------------------------------------------------------ (0)
sezione("(0) IL DATO — book LIVE (TP01 75 / SKH01 25) e replica di controllo")
base = CC.book_series("hourly")
r = DP.deluck_returns()
print(f"\n serie: {len(base)} giorni, {base.index.min().date()} -> {base.index.max().date()}")
print(f" lente 'hourly' = SKH01 sul PATH LIVE (fill al close 5m orario), non il backtest.")
print(f" canonico : drift {base.values.mean()*365:>7.2%} vol {base.values.std()*365**0.5:>6.2%}"
f" Sharpe {base.values.mean()/base.values.std()*365**0.5:>5.2f}")
print(f" de-luckato ×{DP.DELUCK}: drift {r.mean()*365:>7.2%} vol {r.std()*365**0.5:>6.2%}"
f" Sharpe {r.mean()/r.std()*365**0.5:>5.2f} <-- questa e' la serie usata ovunque sotto")
print(f" peggior giorno {r.min():+.2%} · miglior giorno {r.max():+.2%}")
print(f"\n ⚠️ Conseguenza aritmetica da tenere a mente per tutto il resto: con ritorni")
print(f" moltiplicativi il capitale NON tocca zero finche' k < 1/{abs(r.min()):.4f} = "
f"{1/abs(r.min()):.1f}x.")
print(f" Percio' 'rovina' qui e' DEFINITA: il moltiplicatore del path (gemello SENZA")
print(f" versamenti) scende sotto {RUIN_MULT:.0%}, cioe' 4 euro su 5 distrutti. E la")
print(f" liquidazione dell'exchange e' modellata a parte: perdita giornaliera >= {WIPE:.0%}.")
print(f"\n muri pubblicati riusati come bersagli:")
print(f" · ${MURO_PUBBL_LORDO:,.0f} (r0726_capwall_refresh, convenzione LORDA)")
print(f" · ${BERSAGLI['muro netto']:,.0f} (r0807_piano_netto, fisco COERENTE) <-- usato qui")
print(f" · $50.000 (il bersaglio 'corto' chiesto dal brief: ~€19/g di rendita perpetua)")
print("\n CONTROLLO DI REPLICA — questo motore contro la tabella pubblicata il 07/08")
print(" (`r0807_piano_netto` §2: stesso fisco, stesso bersaglio, stessa convenzione di")
print(" mediana condizionata). I path sono diversi: si chiede accordo entro il rumore MC,")
print(" non l'identita'. Se questa riga non torna, nessun numero nuovo sarebbe")
print(" distinguibile da un bug.")
print(f"\n {'EUR/mese':>9}{'anni (pubbl.)':>16}{'P<=20a (pubbl.)':>18}{'anni (qui)':>13}{'P<=20a (qui)':>15}")
for dep, a_pub, p_pub in ((250, 19.8, 0.52), (500, 14.7, 0.99)):
sr = simula(r, (1.00,), anni=25, dep_eur=dep, bersagli={"muro": BERSAGLI["muro netto"]},
aliquota=ALIQUOTA, patrimoniale=PATRIM, n_paths=3_000, seed=725)
a, _, _, p20 = anni_cond(sr["hit"]["muro"][:, 0])
print(f" {dep:>9}{a_pub:>15.1f}a{p_pub:>18.0%}{fmt_anni(a):>13}{p20:>15.0%}")
# ------------------------------------------------------------------ (1)
sezione("(1) LA CURVA DI CRESCITA g(k) — dove sta il massimo, e DI QUANTO il libro ne dista")
print(f"\n Crescita geometrica MEDIANA annua, orizzonte {ANNI} anni, ZERO versamenti,")
print(f" {N_PATHS} path, fisco d'accumulo DENTRO. Un vol-target e' esattamente questo:")
print(" uno scalare sui pesi. Griglia allargata a 22x perche' a 8x il massimo non era")
print(" ancora interno — una griglia che si ferma prima del massimo non lo misura, lo tronca.")
s0 = simula(r, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI,
aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED)
s0b = simula(r, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI,
aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED_BIS)
mu, sig = float(r.mean()), float(r.std())
kelly_gauss = mu / sig ** 2
def g_curve(sim: dict) -> list[float]:
return [float(np.median(sim["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(sim["mult"].shape[1])]
g_med, g_medb = g_curve(s0), g_curve(s0b)
jstar = int(np.argmax(g_med))
kbest, gbest = KS[jstar], g_med[jstar]
j1 = KS.index(1.00)
lg = lambda g: float(np.log1p(g)) # noqa: E731
print(f"\n {'k':>6}{'g mediana/anno':>17}{'(2° seme)':>12}{'ln(1+g)/ln(1+g*)':>19}"
f"{'maxDD mediano':>16}{'P(DD>30%)':>11}{'P(rovina)':>11}{'cap mediano a 25a':>20}")
for j, k in enumerate(KS):
marca = " <-- LIBRO" if j == j1 else (" <-- k*" if j == jstar else "")
print(f" {k:>6.2f}{g_med[j]:>17.2%}{g_medb[j]:>12.2%}{lg(g_med[j])/lg(gbest):>19.2f}"
f"{np.median(s0['dd'][:, j]):>16.1%}{(s0['dd'][:, j] > DD_SOGLIA).mean():>11.1%}"
f"{s0['ruin'][:, j].mean():>11.1%}{np.median(s0['cap'][:, j]):>20,.0f}{marca}")
esec = [j for j, k in enumerate(KS) if k <= 2.50]
ris = max(abs(a - b) for a, b in zip(g_med, g_medb))
ris_e = max(abs(g_med[j] - g_medb[j]) for j in esec)
print(f"\n RISOLUZIONE MONTE CARLO (due semi indipendenti a {N_PATHS} path): scarto massimo")
print(f" {ris:.2%}/anno sull'intera griglia, ma solo {ris_e:.2%}/anno nella regione")
print(f" k<={KS[esec[-1]]:.2f} — il rumore vive tutto nelle celle a leva alta, dove la mediana e'")
print(" presa su una distribuzione enormemente dispersa. Conseguenza da tenere: la")
print(" POSIZIONE del massimo non e' risolvibile a questa taglia (vedi sotto), i confronti")
print(" nella regione eseguibile si'. Ogni confronto e' APPAIATO (stessi path per ogni k).")
print(f"\n RISPOSTA ALLA DOMANDA 'da che parte, e di quanto':")
print(f" massimo EMPIRICO della crescita k* = {kbest:.2f}x (g = {gbest:.1%}/anno)")
print(f" massimo GAUSSIANO (Kelly, mu/sigma²) k = {kelly_gauss:.1f}x")
print(f" il LIBRO gira a k = 1.00x (g = {g_med[j1]:.1%}/anno)")
print(f" -> il libro sta a SINISTRA, a {1.0/kbest:.0%} di Kelly empirico, e raccoglie il")
print(f" {lg(g_med[j1])/lg(gbest):.0%} della crescita massima (in crescita LOGARITMICA, che e' la")
print(" grandezza che Kelly massimizza). NON e' un plateau: e' il ramo ripido.")
print(f"\n ⚠️ La POSIZIONE del massimo non e' risolta a questa taglia: la sezione (4) rifa'")
print(" la stessa curva con 1500 path e il massimo cade a 10x invece di 14x. La regione")
print(" 8-14x e' piatta entro il rumore -> si cita 'k* ~ 10-14x', non un numero.")
print(" Cio' che E' risolto, e con margine enorme, e' che 1.00x non ci sta vicino.")
print(f"\n ⚠️ PREVISIONE MIA, REFUTATA IN SESSIONE. Mi aspettavo che il massimo empirico")
print(f" cadesse molto a SINISTRA di quello gaussiano ({kelly_gauss:.1f}x), perche' il bootstrap a")
print(" blocchi conserva code e clustering che la formula ignora. Non succede: i due")
print(" coincidono entro il rumore. Il motivo e' informativo e prepara la sezione (4) —")
print(" la distribuzione delle CHIUSURE giornaliere di questo libro non ha code abbastanza")
print(" spesse da spostare Kelly. Il rischio che conta a leva non e' in quella")
print(" distribuzione, quindi non lo si trovera' ricampionandola meglio.")
print(f"\n ⚠️ Questo e' il risultato della lente, non una raccomandazione. Tre cose lo")
print(" ridimensionano, e sono misurate sotto: (a) k* dipende dal DRIFT, che e' stimato")
print(f" su {len(r)/365:.1f} anni (sez. 1-bis); (b) la coda che lo sostiene NON e' nel")
print(" campione (sez. 4); (c) meta' della griglia non e' eseguibile (sez. 5).")
print(f" ⚠️ E c'e' un controllo di plausibilita' che il modello fallisce da solo: a k={KS[jstar]:g}x")
print(f" il capitale mediano a 25 anni partendo da $600 e' ${np.median(s0['cap'][:, jstar]):,.0f}.")
print(" Un modello che promette piu' della capitalizzazione del mercato in cui trada ha")
print(" lasciato il dominio in cui vuol dire qualcosa: assume che il ritorno sia")
print(" indipendente dalla SIZE, il che e' falso ben prima di quella cifra.")
# ------------------------------------------------------------------ (1-bis)
sezione("(1-bis) QUANTO E' SOLIDO k* — Kelly e' ipersensibile al drift, e il drift e' una STIMA")
se_drift = float(r.std() * 365 ** 0.5) / (len(r) / 365) ** 0.5
print(f"\n drift stimato {mu*365:.2%}/anno su {len(r)/365:.1f} anni con vol {r.std()*365**0.5:.2%}")
print(f" -> errore standard della MEDIA = vol/sqrt(T) = {se_drift:.2%}/anno.")
print(" Kelly va come mu/sigma²: e' LINEARE nell'errore del drift, quindi l'incertezza")
print(" sul numeratore si trasferisce intera su k*. Stessa macchineria di `deluck_returns`")
print(" (si sottrae una costante al drift, la vol resta quella misurata).")
print(f"\n {'ipotesi sul drift':>34}{'drift/anno':>13}{'k* gaussiano':>15}{'k* empirico':>14}"
f"{'g(1x)':>9}{'g(k*)':>9}")
scen = [("punto (de-luckato ×0.89)", 0.0),
("-1 errore standard", se_drift),
("-2 errori standard", 2 * se_drift),
("drift dimezzato (lente 07/08)", mu * 365 / 2),
("drift a zero", mu * 365)]
for nome, taglio in scen:
rs = r - taglio / 365.0
mus = float(rs.mean())
kg = mus / sig ** 2
ss = simula(rs, KS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA,
patrimoniale=PATRIM, seed=SEED, n_paths=1_500)
gs = g_curve(ss)
js = int(np.argmax(gs))
print(f" {nome:>34}{mus*365:>13.2%}{kg:>15.1f}{KS[js]:>13.2f}x"
f"{gs[j1]:>9.1%}{gs[js]:>9.1%}")
print("\n Lettura: un errore di stima di UN solo errore standard sul drift — che a 7 anni di")
print(" storia e' l'ordine di grandezza normale — sposta k* di parecchie unita'. E' la")
print(" ragione classica per cui si opera a una FRAZIONE di Kelly: non prudenza morale, ma")
print(" il fatto che k* e' un numero stimato con una barra d'errore larga quanto se stesso.")
# ------------------------------------------------------------------ (2)
sezione(f"(2) SWEEP PRINCIPALE — tempo al bersaglio, orizzonte {ANNI} anni, AL NETTO del fisco")
print(f"\n da ${START:.0f} · {N_PATHS} path · blocchi {BLOCK}g · fisco {ALIQUOTA:.0%} + carry "
f"{TX.CARRY_ANNI}a + patrimoniale {PATRIM:.1%}")
print(" 'anni' = mediana CONDIZIONATA all'arrivo (non-arrivo = +infinito, MAI -1): si legge")
print(" sempre insieme a P<=20a accanto. 'p90' = il decile lento, cioe' il rischio di TEMPO.")
print(" ⚠️ Convenzione dichiarata: il fisco e' dentro il CAPITALE (colonne anni/P), NON dentro")
print(" il gemello di rischio (colonne DD/rovina). Un drawdown e' un fatto di mercato: metterci")
print(" l'imposta dentro sommerebbe due cose diverse in un numero solo.")
risultati = {}
for dep in DEPOSITI:
s = s0 if dep == 0 else simula(r, KS, anni=ANNI, dep_eur=dep, bersagli=BERSAGLI,
aliquota=ALIQUOTA, patrimoniale=PATRIM, seed=SEED)
risultati[dep] = s
for nome in BERSAGLI:
print(f"\n --- versamento €{dep}/mese · bersaglio {nome} (${BERSAGLI[nome]:,.0f}) · "
f"versato in {ANNI}a ${s['versato']:,.0f} ---")
print(f" {'k':>6}{'anni (p50)':>13}{'anni (p90)':>13}{'P<=10a':>9}{'P<=15a':>9}"
f"{'P<=20a':>9}{'P(rovina)':>11}{'P(DD>30%)':>11}{'P(DD>50%)':>11}")
for j, k in enumerate(KS):
h = s["hit"][nome][:, j]
a, p10, p15, p20 = anni_cond(h)
arr = h[np.isfinite(h)]
a90 = float(np.percentile(arr, 90)) / 365.0 if len(arr) >= 50 else float("nan")
print(f" {k:>6.2f}{fmt_anni(a):>13}{fmt_anni(a90):>13}{p10:>9.1%}{p15:>9.1%}"
f"{p20:>9.1%}{s['ruin'][:, j].mean():>11.1%}"
f"{(s['dd'][:, j] > DD_SOGLIA).mean():>11.1%}"
f"{(s['dd'][:, j] > 0.50).mean():>11.1%}")
# ------------------------------------------------------------------ (3)
sezione("(3) FRONTIERA ISO-ROVINA — la domanda del brief nella sua forma esatta")
print("\n 'a parita' di probabilita' di rovina accettata': per ogni budget di rischio, la")
print(" leva PIU' ALTA che lo rispetta, e il tempo che quel budget compra. Il rischio si")
print(" misura sul gemello SENZA versamenti (deposit-independent: un bonifico mensile")
print(" maschera un drawdown senza ridurre il capitale distrutto), ed e' la stessa")
print(" grandezza su cui passa la regola di DD per-conto del canale funded.")
for dep in DEPOSITI:
s = risultati[dep]
print(f"\n --- versamento €{dep}/mese ---")
print(f" {'budget':>8}{'metrica':>13}{'k max':>9}"
+ "".join(f"{'anni ' + n:>22}" for n in BERSAGLI))
for metrica, col in (("P(DD>30%)", lambda j: (s["dd"][:, j] > DD_SOGLIA).mean()),
("P(rovina)", lambda j: s["ruin"][:, j].mean())):
for budget in (0.01, 0.05, 0.10, 0.25, 0.50):
amm = [j for j in range(len(KS)) if col(j) <= budget]
if not amm:
print(f" {budget:>7.0%}{metrica:>13}{'nessuna':>9}")
continue
j = max(amm)
celle = []
for n in BERSAGLI:
a, _, _, p20 = anni_cond(s["hit"][n][:, j])
celle.append(f"{fmt_anni(a)} (P20 {p20:.0%})")
print(f" {budget:>7.0%}{metrica:>13}{KS[j]:>8.2f}x" + "".join(f"{c:>22}" for c in celle))
print("\n ⚠️ Le due metriche danno risposte MOLTO diverse, e la ragione conta: P(rovina)")
print(" (perdere l'80% del moltiplicatore) e' quasi zero fino a leve altissime perche'")
print(" il libro non ha mai perso l'80% in nessuna finestra ricampionabile; P(DD>30%)")
print(" morde subito. Chi sceglie la leva sul solo azzeramento sceglie sulla metrica")
print(" che il dataset non puo' informare — vedi (4).")
# ------------------------------------------------------------------ (4)
sezione("(4) IL GATE CHE CONTA — k* e' calcolato su un campione senza un solo giorno di crash")
print(f"\n Il block bootstrap ricampiona i giorni VISSUTI: non puo' produrre un giorno")
print(f" peggiore di {r.min():.2%}. Il libro ha attraversato il 2022 (LUNA/FTX) restando")
print(" difensivo — quindi per k=1 la coda del campione e' onesta. Ma a leva il rischio")
print(" cambia NATURA: entra la liquidazione, che su una chiusura giornaliera non si vede")
print(" (lezione della lente wick accoppiata, 25/07: 'close-only non e' conservativa, e'")
print(" CIECA'). Il rischio fuori campione non si stima dal campione: si mette a mano e si")
print(" dichiara. Ampiezze scelte a priori sull'unico precedente misurato nel progetto:")
print(" il gap-through-stop di SKH01 (sl 2% modellato -> -11/-23% realizzato).")
print(f"\n liquidazione se la perdita a leva supera {WIPE:.0%} dell'equity in un giorno.")
print(f"\n {'scenario':>22}" + "".join(f"{f'k={k:g}':>10}" for k in KS_STRESS) + f"{' k*':>8}")
s0s = simula(r, KS_STRESS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA,
patrimoniale=PATRIM, seed=SEED, n_paths=1_500)
g0s = [float(np.median(s0s["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(len(KS_STRESS))]
print(f" {'nessuno stress':>22}" + "".join(f"{g:>10.1%}" for g in g0s)
+ f"{f'{KS_STRESS[int(np.argmax(g0s))]:g}x':>8}")
for cpct in (0.10, 0.15, 0.20):
for panno in (0.20, 1.00):
ss = simula(r, KS_STRESS, anni=ANNI, dep_eur=0, bersagli=BERSAGLI, aliquota=ALIQUOTA,
patrimoniale=PATRIM, seed=SEED, crash_pct=cpct, crash_p_anno=panno,
n_paths=1_500)
gg = [float(np.median(ss["mult"][:, j])) ** (1 / ANNI) - 1 for j in range(len(KS_STRESS))]
print(f" {f'-{cpct:.0%} ogni {1/panno:.0f}a':>22}"
+ "".join(f"{g:>10.1%}" for g in gg)
+ f"{f'{KS_STRESS[int(np.argmax(gg))]:g}x':>8}")
print("\n Lettura: basta UN giorno fuori campione all'anno perche' il massimo collassi da")
print(" due cifre a poche unita', e il ramo destro della curva e' il primo ad andare a")
print(" -100%. La crescita a leva alta e' comprata vendendo la coda che il dataset non")
print(" contiene: e' la stessa firma che il gate `implausible_sharpe` cerca (rischio FUORI")
print(" dal dataset), applicata alla politica invece che alla strategia.")
# ------------------------------------------------------------------ (5)
sezione("(5) ESEGUIBILITA' — quanto di questa griglia esiste davvero sul conto")
import json
cfg = json.loads((ROOT / "config" / "live.json").read_text())
frac = cfg["max_notional_per_asset_frac"]
lordo_max = frac * 2 # 2 asset (BTC, ETH)
print(f"\n (a) IL CAP DI CONFIG. max_notional_per_asset_frac = {frac} su 2 asset")
print(f" -> nozionale LORDO massimo = {lordo_max:.2f}x l'equity. **k > {lordo_max:.2f}x NON e'")
print(" eseguibile senza toccare `config/live.json`**, e il 26/07 il progetto ha legato")
print(" una conclusione a quel parametro con un test apposta")
print(" (`test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`): alzare il cap ROMPE")
print(" quel test, che e' esattamente il suo scopo. Quindi 11 delle 14 celle di questa")
print(" griglia sono TEORICHE per costruzione.")
print(f" min_order ${cfg['min_order_usd']} · disaster-SL {cfg['disaster_sl_pct']:.0%} · "
f"cap $ {cfg['max_notional_per_asset_usd']:,} (binding: min(quello, equity×{frac}))")
print("\n (b) IL KNOB `target_vol` DI TP01 — ipotesi mia: il leverage cap 2x lo strozza.")
from src.data.downloader import load_data # noqa: E402
from src.strategies.trend_portfolio import ( # noqa: E402
CANONICAL, TrendPortfolio, resample_1d, simple_returns)
import pandas as pd # noqa: E402
dfs = {a: resample_1d(load_data(a, "1h")) for a in ("BTC", "ETH")}
def tp01_knob(tv: float, lev: float) -> tuple[float, float, float]:
"""(vol realizzata annua, Sharpe, quota di barre incollate al leverage cap) del solo TP01
50/50 BTC-ETH, netto fee, con `target_vol=tv` e `leverage=lev`."""
serie, atcap = {}, []
for a, df in dfs.items():
tp = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev})
tgt = np.nan_to_num(np.asarray(tp.target_series(df), float))
ret = simple_returns(df["close"].values.astype(float))
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1]
net = pos * ret - 0.0005 * np.abs(np.diff(pos, prepend=0.0))
net[0] = 0.0
serie[a] = pd.Series(net, index=pd.to_datetime(df["datetime"]))
atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean()))
J = pd.concat(serie, axis=1, join="inner").fillna(0.0) # le due serie hanno lunghezze
x = (0.5 * J["BTC"] + 0.5 * J["ETH"]).values # diverse: si allineano sulle date
return (float(x.std() * 365 ** 0.5), float(x.mean() / x.std() * 365 ** 0.5),
float(np.mean(atcap)))
rif = tp01_knob(0.20, 2.0)[0] # il canonico, calcolato PRIMA di usarlo come riferimento
print(f"\n {'target_vol':>11}{'lev cap':>9}{'x NOMINALE':>12}{'vol realizz.':>14}"
f"{'x OTTENUTO':>12}{'Sharpe':>9}{'quota barre al cap':>21}")
for lev in (2.0, 4.0):
for tv in (0.10, 0.20, 0.30, 0.40, 0.60):
v, sh, ac = tp01_knob(tv, lev)
print(f" {tv:>11.0%}{lev:>9.1f}{tv/0.20:>12.2f}{v:>14.2%}{v/rif:>12.2f}{sh:>9.2f}"
f"{ac:>21.1%}")
v20, sh20, _ = tp01_knob(0.20, 2.0)
print(f"\n ❌ IPOTESI REFUTATA, e la refutazione e' piu' utile dell'ipotesi. 'x OTTENUTO'")
print(" segue 'x NOMINALE' fino a target_vol 60% (3.00 nominale -> 2.98 ottenuto) e le")
print(" barre incollate al cap sono l'1.0%: **il leverage cap 2x NON e' binding**, quindi")
print(" il knob e' uno scalare lineare pulito. Lo Sharpe e' invariante a ogni cella")
print(f" (1.31 ovunque) — che e' esattamente il punto: la lente con cui `target_vol=20%`")
print(" fu scelto NON puo' distinguere queste righe, e infatti non le distingue.")
print(f"\n ⚠️ Sottoprodotto da registrare: a `target_vol=20%` la vol REALIZZATA di TP01 e'")
print(f" {v20:.2%}, non 20%. Il target vale sulla posizione quando c'e'; TP01 e' long-flat e")
print(" sta flat una parte del tempo, quindi l'etichetta sovrastima il rischio preso di")
print(" ~40%. Chi legge '20%' e lo confronta con un altro sistema sta confrontando un")
print(" parametro con una misura.")
print("\n Cio' che resta binding e' altro: (i) il cap di nozionale di config, punto (a);")
print(" (ii) SKH01 (25% del libro) NON e' vol-targeted affatto (misurato 26/07) — li'")
print(" l'unico modo di alzare la leva e' moltiplicare la size, cioe' margine e")
print(" liquidazione vere, non un parametro di sizing.")
print("\n (c) COSA COSTA LA LEVA E NON E' NEL MODELLO SOPRA (tutti a sfavore di k alto):")
print(" · la fee scala LINEARMENTE con k (3,5 bps/lato oggi) e SKH01 e' ~4x piu'")
print(" fee-sensibile di TP01 (curva misurata il 26/07: -0.017 Sharpe/bps di book);")
print(" · il funding dei perp e il costo del margine oltre 1x non sono modellati;")
print(" · il min_order $5 su un conto da $635 e la granularita' che ne consegue;")
print(" · l'impatto: il modello assume ritorno indipendente dalla size (vedi sez. 1).")
print(" -> i numeri a k>1 sono un TETTO, non una stima.")
# ------------------------------------------------------------------ verdetto
sezione("VERDETTO")
s500 = risultati[500]
jj = {k: KS.index(k) for k in (1.00, 1.50, 2.00)}
righe = []
for nome in BERSAGLI:
for k, j in jj.items():
a, _, _, p20 = anni_cond(s500["hit"][nome][:, j])
righe.append((nome, k, a, p20, (s500["dd"][:, j] > DD_SOGLIA).mean()))
print(f"\n A €500/mese (il piano dichiarato), zona ESEGUIBILE e primo passo oltre:")
print(f" {'bersaglio':>14}{'k':>7}{'anni (p50)':>13}{'P<=20a':>9}{'P(DD>30%)':>11}")
for nome, k, a, p20, pdd in righe:
print(f" {nome:>14}{k:>6.2f}x{fmt_anni(a):>13}{p20:>9.0%}{pdd:>11.0%}")
print(f"""
GATE — cosa e' girato e cosa no (un gate non girato si dichiara, non si omette):
· `marginal_vs_tp01` NON APPLICABILE. Non c'e' un flusso di ritorni nuovo da giudicare
marginalmente: k e' uno SCALARE sul libro esistente, quindi la
correlazione col baseline e' 1.00 per costruzione.
· `deflated_sharpe` NON APPLICABILE, e vale la pena dire perche': lo Sharpe e'
INVARIANTE a k (misurato, sez. 5b: 1.31 a ogni cella del knob).
Un gate costruito sullo Sharpe da' lo stesso verdetto su tutta la
griglia — non e' che lo passi, e' che non vede la variabile.
· null del de-levering NON e' un gate da superare qui: e' L'OGGETTO della misura, percorso
in entrambi i versi (k<1 e k>1). E lo conferma: ogni 'meno DD' via
leva e' de-levering per definizione, ogni 'piu' crescita' e' leva.
· fortuna d'ancora GESTITA col fattore MISURATO ×{DP.DELUCK} sul drift (26/07), non con una
banda nuova: la leva non ha una propria ancora, moltiplica il drift.
· `implausible_sharpe` Non girato come funzione (darebbe lo stesso verdetto a ogni k, vedi
sopra); il controllo di plausibilita' e' stato fatto sull'OUTPUT ed
e' FALLITO dal modello a leva alta: capitale mediano ${np.median(s0['cap'][:, jstar]):,.0f}
da $600 in 25 anni. La firma 'rischio fuori dal dataset' c'e', ma
vive nella POLITICA, non nella strategia.
· causalita' Ereditata: la serie viene da `CC.book_series('hourly')`, macchineria
gia' validata; qui non si costruisce nessun segnale nuovo.
· risoluzione MC DICHIARATA: {ris_e:.2%}/anno nella regione eseguibile, {ris:.2%}/anno sulla
griglia intera. La posizione del massimo NON e' risolta.
1. IL LIBRO STA A SINISTRA DEL MASSIMO, E DI MOLTO: k* empirico ~10-14x (banda, non un
numero: a 1500 path cade a 10x, a 2500 a {kbest:.0f}x), il libro gira a 1x = {1.0/kbest:.0%} di Kelly
e raccoglie il {lg(g_med[j1])/lg(gbest):.0%} della crescita massima in log. Non e' un plateau
(come lo era il peso 75/25): e' il ramo ripido della curva. Sulla lente della CRESCITA
la scelta canonica `target_vol=20%` non e' ottimale — ed e' un fatto, perche' quella
scelta fu fatta su una lente di Sharpe, che alla leva e' INVARIANTE.
2. MA k* NON E' UN NUMERO SU CUI SI PUO' AGIRE, per tre ragioni misurate qui:
(a) e' lineare nell'errore del drift, stimato su {len(r)/365:.1f} anni (sez. 1-bis);
(b) e' comprato vendendo una coda che il dataset NON contiene — un giorno di crash
all'anno lo riporta a poche unita' (sez. 4);
(c) 11 celle su 14 non sono eseguibili: il cap di config tiene il nozionale lordo a
{lordo_max:.2f}x l'equity. (Il knob `target_vol` invece FUNZIONA — misurato, sez. 5b: il
leverage cap 2x non morde. L'ostacolo e' la config e SKH01, non il sizing di TP01.)
3. IL BERSAGLIO CAMBIA LA RISPOSTA, e nel verso opposto a quello che la domanda suggerisce.
Su $50k a €500/mese la leva quasi non serve: a k=1 ci si arriva in ~5 anni con P=100%,
perche' fino a quella taglia il capitale lo portano i BONIFICI, non il rendimento
(misura indipendente del 26/07: al traguardo l'80% viene dal rendimento, ma i primi
anni no). Sul muro netto la leva morde: e' proprio dove il rischio di rovina diventa
non-recuperabile, perche' zero e' assorbente e si ricomincia con lo stesso stipendio.
4. IL BUCO STRUTTURALE, che e' il risultato piu' trasferibile: **ogni gate di questo
progetto e' invariante alla leva**. `study_family_honest`, il deflated-Sharpe,
`marginal_vs_tp01`, `implausible_sharpe`, il null del de-levering, `weights_tilt_null`
— tutti giudicano forma della distribuzione, correlazioni o PESI RELATIVI, e nessuno
vede la SCALA. Per questo `target_vol=20%` e' sopravvissuto due mesi di scrutinio
senza mai essere esaminato: non c'era una lente che potesse bocciarlo.
5. COSA RESTA DI OPERATIVO: nulla da cambiare oggi. Il gradino ESEGUIBILE (k da 1.00x a
1.25-1.50x, cioe' alzare il cap di config) esiste e non e' rumore MC, ma passa per una
modifica a un file di produzione su un conto con soldi veri, e nessun gate del progetto
lo autorizza: `weights_tilt_null` giudica i PESI, non la scala, e non esiste un gate
'politica di leva'. Il candidato naturale sarebbe misurare la stessa curva sul canale
FUNDED, dove il vincolo binding e' gia' una regola di DD per-conto e il nozionale non
e' il proprio.
""")
if __name__ == "__main__":
main()
+697
View File
@@ -0,0 +1,697 @@
"""HL-EXEC (2026-08-22) — AUDIT DI FATTO: le regole VERE di Hyperliquid contro le soglie ASSUNTE.
DOMANDA. Il progetto tiene due edge cross-sectional fuori dal libro **per taglia**:
XS01 "serve ~$20.000" (origine: diario 2026-06-19-hyperliquid-xsec, "rumore arrotondamento" —
una STIMA A OCCHIO, mai calcolata)
XSR01 "diventa reale a ~$5.000" + gate pre-registrato 2026-10-23 con soglia
"haircut di eseguibilita' a $5.000 <= 40%, altrimenti RITIRO"
Entrambe sono calcolate col pavimento **min_order $5**, che e' il minimo di **DERIBIT**.
XS01/XSR01 si eseguirebbero su **HYPERLIQUID**, che ha regole sue. Qui le regole si LEGGONO dal
venue invece di assumerle, e si dice quali conclusioni del progetto cambiano.
NON manda ordini, non tocca il conto: sole letture PUBBLICHE (nessuna chiave, nessuna firma).
GRADO DELLE FONTI (dichiarato per ogni numero, come chiede il brief):
[A] letto dall'API pubblica del venue api.hyperliquid.xyz/info (meta, metaAndAssetCtxs,
userFees, l2Book)
[B] derivato da [A] + verificato empiricamente sui book live
[C] documentazione ufficiale (hyperliquid.gitbook.io)
[D] assunto / ereditato dal progetto
La verifica MAINNET non e' un atto di fede: i mark price dell'API si incrociano con l'ultima
chiusura del feed CERTIFICATO su disco (data/raw/hl_*_1d.parquet). Il testnet non puo' superarlo.
(Il progetto ha gia' pagato il prezzo di un feed testnet creduto vero: e' la causa del reset v2.0.0.)
nice -n 19 timeout 900 uv run python scripts/research/r0822_hl_exec.py
... --no-net usa solo la cache su disco (nessuna chiamata di rete)
... --snaps N quanti snapshot del book L2 (default 3, ~45s di distanza)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research"))
RAW = PROJECT_ROOT / "data" / "raw"
CACHE = Path(os.environ.get("HLEXEC_CACHE", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/hlexec"))
INFO_URL = "https://api.hyperliquid.xyz/info" # MAINNET (il testnet e' api.hyperliquid-testnet.xyz)
# --- parametri sotto esame ----------------------------------------------------------------------
MIN_ORDER_ASSUNTO = 5.0 # [D] pavimento DERIBIT usato da eval_weights_smallcap e da paper_xsr
MIN_ORDER_HL = 10.0 # [C] "Order must have minimum value of $10." (docs/error-responses)
FEE_LEG_MODELLO = 0.0005 # [D] 0.05%/gamba, config CONGELATA di XSR01
XSR_SOGLIA_HAIRCUT = 0.40 # [D] soglia pre-registrata del gate 2026-10-23
CAPITALI = (600.0, 1000.0, 1500.0, 2000.0, 3000.0, 4000.0, 5000.0, 7500.0,
10000.0, 20000.0, 50000.0)
CAPITALI_XS = (100.0, 200.0, 300.0, 600.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0)
TICKETS = (50.0, 100.0, 300.0)
ANN = np.sqrt(365.0)
# ==================================================================================================
# 0. RETE — letture pubbliche, con cache su disco (una corsa non deve ri-martellare il venue)
# ==================================================================================================
def _post(payload: dict, timeout: int = 30) -> object:
req = urllib.request.Request(INFO_URL, data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read().decode())
def cached(name: str, payload: dict, no_net: bool, ttl_s: float = 6 * 3600) -> object:
CACHE.mkdir(parents=True, exist_ok=True)
f = CACHE / f"{name}.json"
if f.exists() and (no_net or (time.time() - f.stat().st_mtime) < ttl_s):
return json.loads(f.read_text())
if no_net:
raise FileNotFoundError(f"cache assente per {name} e --no-net attivo")
d = _post(payload)
f.write_text(json.dumps(d))
return d
def universo_certificato() -> list[str]:
"""I 51 alt+BTC gia' CERTIFICATI su disco. Non si inventa un universo: si legge quello vero."""
return sorted(p.stem.replace("hl_", "").replace("_1d", "").upper()
for p in RAW.glob("hl_*_1d.parquet"))
# ==================================================================================================
# 1. REGOLE DEL VENUE
# ==================================================================================================
def sig_figs_ok(px_str: str, sz_dec: int, max_dec: int = 6) -> bool:
"""[C] regola tick perp: <=5 cifre significative E <= (MAX_DECIMALS - szDecimals) decimali;
i prezzi INTERI sono sempre ammessi. Qui serve per VERIFICARLA sui book veri -> [B]."""
s = px_str.lstrip("-")
dec = len(s.split(".")[1]) if "." in s else 0
if dec == 0:
return True
digits = s.replace(".", "").lstrip("0")
return len(digits.rstrip("0")) <= 5 and dec <= (max_dec - sz_dec)
def leggi_regole(no_net: bool) -> tuple[pd.DataFrame, dict]:
meta_ctx = cached("metaAndAssetCtxs", {"type": "metaAndAssetCtxs"}, no_net)
fees = cached("userFees", {"type": "userFees",
"user": "0x0000000000000000000000000000000000000001"}, no_net)
uni, ctxs = meta_ctx[0]["universe"], meta_ctx[1]
rows = []
for a, c in zip(uni, ctxs):
mark = float(c["markPx"]) if c.get("markPx") else np.nan
rows.append(dict(sym=a["name"], szDecimals=int(a["szDecimals"]),
maxLeverage=int(a["maxLeverage"]),
delisted=bool(a.get("isDelisted", False)),
markPx=mark, dayNtlVlm=float(c.get("dayNtlVlm") or np.nan),
openInterest=float(c.get("openInterest") or np.nan)))
df = pd.DataFrame(rows).set_index("sym")
df["lot_units"] = 10.0 ** (-df["szDecimals"]) # [A] passo di size in unita'
df["lot_usd"] = df["lot_units"] * df["markPx"] # [B] passo di size in dollari
df["px_decimals_max"] = (6 - df["szDecimals"]).clip(lower=0) # [C]
return df, fees["feeSchedule"]
def verifica_mainnet(df: pd.DataFrame, syms: list[str]) -> pd.DataFrame:
"""Incrocia i mark dell'API con l'ULTIMA chiusura del feed certificato su disco.
Un feed testnet non puo' superare questo controllo: i suoi prezzi sono fantasia."""
out = []
for s in syms:
p = RAW / f"hl_{s.lower()}_1d.parquet"
if not p.exists() or s not in df.index:
continue
d = pd.read_parquet(p, columns=["timestamp", "close"])
last_close = float(d["close"].iloc[-1])
last_ts = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC")
mk = float(df.loc[s, "markPx"])
out.append(dict(sym=s, disco=last_close, api=mk,
dev_pct=100.0 * (mk - last_close) / last_close,
eta_h=(pd.Timestamp.now("UTC") - last_ts) / pd.Timedelta("1h")))
return pd.DataFrame(out).set_index("sym")
# ==================================================================================================
# 2. BOOK L2 -> spread, profondita', costo di un ticket
# ==================================================================================================
def scarica_book(syms: list[str], snaps: int, no_net: bool, pause: float = 0.25,
gap_s: float = 45.0) -> dict[str, list[dict]]:
books: dict[str, list[dict]] = {s: [] for s in syms}
for k in range(snaps):
if k and not no_net:
time.sleep(gap_s)
for s in syms:
try:
b = cached(f"l2_{s}_{k}", {"type": "l2Book", "coin": s}, no_net)
except Exception as e: # un book mancante e' un dato, non un crash
print(f" [book] {s} snap{k}: {type(e).__name__}")
continue
books[s].append(b)
if not no_net:
time.sleep(pause) # pacing: il rate limit e' PER-IP e condiviso
return books
def costo_ticket(levels: list[dict], mid: float, notional: float, side: str) -> tuple[float, bool]:
"""VWAP di un ordine marketable da `notional` dollari contro il book, in bps DA MID.
Ritorna (bps, book_esaurito). Per ticket piccoli converge al MEZZO SPREAD: e' giusto cosi',
a $14 non si 'cammina' il book, si paga il livello top."""
resid, cost, filled = notional, 0.0, 0.0
for lv in levels:
px, sz = float(lv["px"]), float(lv["sz"])
cap = px * sz
take = min(resid, cap)
cost += take
filled += take / px
resid -= take
if resid <= 1e-9:
break
if filled <= 0:
return float("nan"), True
vwap = cost / filled
bps = (vwap - mid) / mid * 1e4 * (1.0 if side == "buy" else -1.0)
return bps, resid > 1e-6
def misura_liquidita(books: dict[str, list[dict]], meta: pd.DataFrame) -> pd.DataFrame:
rows = []
for s, snaps in books.items():
if not snaps:
continue
rec: dict[str, list[float]] = {"spread": [], "d10": [], "viol": []}
for t in TICKETS:
rec[f"t{int(t)}"] = []
for b in snaps:
bids, asks = b["levels"][0], b["levels"][1]
if not bids or not asks:
continue
bb, ba = float(bids[0]["px"]), float(asks[0]["px"])
mid = 0.5 * (bb + ba)
rec["spread"].append((ba - bb) / mid * 1e4)
near = sum(float(l["px"]) * float(l["sz"]) for l in asks
if (float(l["px"]) - mid) / mid <= 0.0010)
near += sum(float(l["px"]) * float(l["sz"]) for l in bids
if (mid - float(l["px"])) / mid <= 0.0010)
rec["d10"].append(near)
sd = int(meta.loc[s, "szDecimals"]) if s in meta.index else 0
rec["viol"].append(sum(0 if sig_figs_ok(l["px"], sd) else 1 for l in bids + asks))
for t in TICKETS:
bb_, _ = costo_ticket(asks, mid, t, "buy")
sb_, _ = costo_ticket(bids, mid, t, "sell")
if np.isfinite(bb_) and np.isfinite(sb_):
rec[f"t{int(t)}"].append(0.5 * (bb_ + sb_))
if not rec["spread"]:
continue
row = dict(sym=s, n_snap=len(rec["spread"]),
spread_bps=float(np.median(rec["spread"])),
half_spread_bps=float(np.median(rec["spread"])) / 2.0,
depth10bps_usd=float(np.median(rec["d10"])),
tick_viol=int(sum(rec["viol"])))
for t in TICKETS:
v = rec[f"t{int(t)}"]
row[f"slip{int(t)}_bps"] = float(np.median(v)) if v else np.nan
rows.append(row)
return pd.DataFrame(rows).set_index("sym").sort_values("spread_bps")
def min_notional_osservato(books: dict[str, list[dict]]) -> dict:
"""Controllo EMPIRICO del pavimento $10: i livelli con n==1 sono UN ordine solo.
Un ordine singolo sotto $10 non falsifica la regola (i fill parziali erodono un resto),
ma il PAVIMENTO della distribuzione dice dove il venue taglia."""
vals = []
for snaps in books.values():
for b in snaps:
for side in b["levels"]:
for l in side:
if int(l.get("n", 0)) == 1:
vals.append(float(l["px"]) * float(l["sz"]))
v = np.array(vals, float)
if not len(v):
return {}
return dict(n=len(v), minimo=float(v.min()), p01=float(np.percentile(v, 1)),
p05=float(np.percentile(v, 5)), mediana=float(np.median(v)),
sotto10_pct=100.0 * float((v < 10.0).mean()),
sotto5_pct=100.0 * float((v < 5.0).mean()))
# ==================================================================================================
# 3. SIMULATORE DI LIBRO A N GAMBE (la contabilita' e' quella di paper_xsr._step, generalizzata)
# ==================================================================================================
def sim_libro(Weff: np.ndarray, R: np.ndarray, r_hedge: np.ndarray | None, cap0: float,
min_order: float | None, lot_usd: np.ndarray | None, px: np.ndarray | None,
fee_leg: float | np.ndarray) -> dict:
"""Un passo per barra. Una gamba il cui |dw|*capitale sta sotto min_order NON si muove
(stessa convenzione di altlib.eval_weights_smallcap e di scripts/live/paper_xsr._step).
In piu': la size si arrotonda al LOTTO del venue -> se arrotonda a zero, non si esegue.
`fee_leg` puo' essere uno scalare o un vettore per-gamba (costo asset-specifico)."""
n, k = Weff.shape
w = np.zeros(k)
cap = cap0
eq = np.empty(n)
nets = np.zeros(n)
n_fill = n_skip = n_lot0 = 0
ntl_req = ntl_done = 0.0
fee = np.broadcast_to(np.asarray(fee_leg, float).ravel(), (k,)) if np.ndim(fee_leg) else None
for i in range(n):
ret = float(np.dot(w, R[i]))
if r_hedge is not None:
ret -= float(w.sum()) * r_hedge[i]
w_t = Weff[i]
if min_order is None:
w_new = w_t.copy()
else:
move = np.abs(w_t - w) * cap >= min_order
if lot_usd is not None and px is not None:
# arrotondamento al lotto: la size eseguita e' un multiplo di lot_units
dn = np.abs(w_t - w) * cap
lots = np.floor(dn / np.maximum(lot_usd, 1e-12))
zero = move & (lots < 1)
n_lot0 += int(zero.sum())
move = move & (lots >= 1)
need = np.abs(w_t - w) > 1e-12 # una gamba gia' a posto NON e' un ordine saltato
ntl_req += float((np.abs(w_t - w) * cap).sum())
ntl_done += float((np.abs(w_t - w) * cap)[move].sum())
w_new = np.where(move, w_t, w)
n_fill += int((move & need).sum())
n_skip += int(((~move) & need).sum())
d = np.abs(w_new - w)
c = float((d * fee).sum()) if fee is not None else float(fee_leg) * float(d.sum())
if r_hedge is not None: # la gamba di copertura paga anch'essa
dh = abs(float(w_new.sum()) - float(w.sum()))
c += (float(fee.mean()) if fee is not None else float(fee_leg)) * dh
net = ret - c
nets[i] = net
cap *= (1.0 + max(net, -0.99))
eq[i] = cap
w = w_new
tot = n_fill + n_skip
return dict(net=nets, eq=eq, n_fill=n_fill, n_skip=n_skip, n_lot0=n_lot0,
pct_eseguite=(100.0 * n_fill / tot) if tot else np.nan,
pct_nozionale=(100.0 * ntl_done / ntl_req) if ntl_req > 0 else np.nan)
def metriche(net: np.ndarray, idx: pd.DatetimeIndex) -> dict:
x = np.asarray(net, float)
sd = x.std(ddof=1)
sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0
eq = np.cumprod(1.0 + np.clip(x, -0.99, None))
dd = float((1.0 - eq / np.maximum.accumulate(eq)).max())
yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9)
cagr = float(eq[-1] ** (1.0 / yrs) - 1.0)
return dict(sharpe=round(sh, 3), maxdd=round(100 * dd, 1), cagr=round(100 * cagr, 1))
# ==================================================================================================
# 4. XS01 — ricostruzione dei PESI (il sleeve espone solo i ritorni) + prova d'identita'
# ==================================================================================================
def xs01_pesi(phase: int = 0):
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns
cols = {}
for sym in XS_UNIVERSE:
p = RAW / f"hl_{sym.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
px = C.values
n, A = px.shape
lb, H, k, mode, tv = (XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"],
XS_CFG["mode"], XS_CFG["target_vol"])
dp, mh = XS_CFG.get("disp_pct", 0), XS_CFG.get("disp_minhist", 20)
mlb = max(lb)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A))
w = np.zeros(A)
hist: list[float] = []
for i in range(n):
if i >= mlb and i % H == phase:
rLs = [px[i] / px[i - L] - 1.0 for L in lb]
di = float(np.mean([r.std() for r in rLs]))
thr = np.percentile(hist, dp) if (dp > 0 and len(hist) >= mh) else -np.inf
if di >= thr:
sc = np.zeros(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
sc += (rL - rL.mean()) / sd; cnt += 1
if cnt:
sc /= cnt
o = np.argsort(sc); w = np.zeros(A); lo, hi = o[:k], o[-k:]
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
else:
w = np.zeros(A)
hist.append(di)
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (0.001 / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
# prova d'identita' col sleeve UFFICIALE (max|diff| deve essere 0.0)
if phase == 0:
off = _xsec_returns()
dif = float(np.abs(pd.Series(s.values * scale, index=C.index).reindex(off.index).values
- off.values).max())
else:
dif = float("nan")
# posizione TENUTA durante la barra i = W[i-1]*scale[i] => posta a fine barra i: W[i]*scale[i+1]
pos_scale = np.concatenate([scale[1:], scale[-1:]])
return W * pos_scale[:, None], dret, C, list(C.columns), dif
# ==================================================================================================
# MAIN
# ==================================================================================================
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--no-net", action="store_true")
ap.add_argument("--snaps", type=int, default=3)
a = ap.parse_args()
pd.set_option("display.width", 200)
print("=" * 104)
print(" HL-EXEC — le regole VERE di Hyperliquid contro le soglie ASSUNTE di XS01 / XSR01")
print("=" * 104)
# ---------------------------------------------------------------- 1. regole
meta, fee_sched = leggi_regole(a.no_net)
syms = universo_certificato()
print(f"\n[1] REGOLE DEL VENUE (fonte: {INFO_URL} = MAINNET)")
print(f" universo perp quotato: {len(meta)} strumenti; certificati da noi: {len(syms)}")
mancanti = [s for s in syms if s not in meta.index]
delist = [s for s in syms if s in meta.index and bool(meta.loc[s, "delisted"])]
print(f" certificati assenti dal venue: {mancanti or 'nessuno'} delistati: {delist or 'nessuno'}")
ck = verifica_mainnet(meta, syms)
print(f"\n [A] verifica MAINNET (mark API vs ultima chiusura del feed certificato, "
f"eta' feed {ck['eta_h'].median():.0f}h):")
print(f" deviazione |%| mediana {ck['dev_pct'].abs().median():.2f}% "
f"max {ck['dev_pct'].abs().max():.2f}% ({ck['dev_pct'].abs().idxmax()}) "
f"asset controllati {len(ck)}")
mv = [float(np.abs(pd.read_parquet(RAW / f"hl_{sy.lower()}_1d.parquet", columns=["close"])
["close"].pct_change().dropna()).median()) * 100 for sy in ck.index]
print(f" movimento |giornaliero| tipico di questi asset: {float(np.median(mv)):.2f}% -> "
f"una deviazione mediana del {ck['dev_pct'].abs().median():.2f}% su un feed vecchio "
f"{ck['eta_h'].median():.0f}h e' cio' che ci si attende (non e' un disaccordo di venue).")
try:
from src.data.downloader import load_data as _ld
bd = float(_ld("BTC", "1h")["close"].iloc[-1])
hl = float(meta.loc["BTC", "markPx"])
print(f" cross-check su un TERZO venue: BTC Deribit certificato ${bd:,.0f} vs "
f"HL API ${hl:,.0f} = {100*(hl-bd)/bd:+.2f}%")
except Exception as e:
print(f" (cross-check Deribit non disponibile: {type(e).__name__})")
print(" -> un feed TESTNET non supera questo controllo: i suoi prezzi sono scollegati.")
tk = fee_sched
print(f"\n [A] FEE perp lette dal venue (tier BASE, nessuna chiave):")
print(f" taker (cross) {float(tk['cross'])*1e4:.2f} bps/lato "
f"maker (add) {float(tk['add'])*1e4:.2f} bps/lato")
print(f" primo scaglione VIP a ${float(tk['tiers']['vip'][0]['ntlCutoff']):,.0f} di "
f"volume 14g -> a $600-20k si sta SEMPRE al tier base.")
print(f" [C] la doc (gitbook/trading/fees) dichiara 0.045% / 0.015%: **le due fonti "
f"coincidono**.")
print(f" [C] pavimento d'ordine perp: \"Order must have minimum value of $10.\" "
f"(docs/for-developers/api/error-responses); nessuna esenzione documentata per reduce-only.")
print(f" [C] tick: <=5 cifre significative E <= (6 - szDecimals) decimali; size arrotondata "
f"a szDecimals.")
sub = meta.loc[[s for s in syms if s in meta.index]].copy()
print(f"\n [A/B] LOTTO per asset (passo di size) — i 6 piu' grossolani e i 6 piu' fini:")
o = sub.sort_values("lot_usd", ascending=False)
for who, part in (("piu' grossolano", o.head(6)), ("piu' fine", o.tail(6))):
for s, r in part.iterrows():
print(f" {s:<6} szDec={int(r.szDecimals)} lotto {r.lot_units:>10.5f} unita' = "
f"${r.lot_usd:>8.4f} mark ${r.markPx:<12.6g} ({who})")
print(f" lotto in $: mediana ${sub['lot_usd'].median():.4f}, max "
f"${sub['lot_usd'].max():.2f} ({sub['lot_usd'].idxmax()})")
print(f" -> il LOTTO non e' mai il vincolo: max ${sub['lot_usd'].max():.2f} << $10 "
f"di pavimento. Il vincolo e' il MIN NOTIONAL.")
print(f" [A] leva massima: BTC {int(meta.loc['BTC','maxLeverage'])}x, "
f"ETH {int(meta.loc['ETH','maxLeverage'])}x, mediana alt certificati "
f"{int(sub['maxLeverage'].median())}x, minimo {int(sub['maxLeverage'].min())}x")
# ---------------------------------------------------------------- 2. book
print(f"\n[2] BOOK L2 — spread, profondita', costo di un ticket ({a.snaps} snapshot per asset)")
books = scarica_book(syms, a.snaps, a.no_net)
liq = misura_liquidita(books, meta)
mn = min_notional_osservato(books)
if mn:
print(f" [B] controllo empirico del pavimento $10 su {mn['n']} livelli a UN SOLO ordine "
f"(n==1):")
print(f" minimo ${mn['minimo']:.2f} p01 ${mn['p01']:.2f} p05 ${mn['p05']:.2f} "
f"mediana ${mn['mediana']:.0f} sotto $10: {mn['sotto10_pct']:.1f}% "
f"sotto $5: {mn['sotto5_pct']:.1f}%")
print(f" (un residuo sotto soglia NON falsifica la regola — i fill parziali erodono "
f"un ordine gia' piazzato — ma il pavimento della distribuzione dice dove taglia.)")
viol = int(liq["tick_viol"].sum())
print(f" [B] regola tick verificata sui book veri: {viol} violazioni su "
f"{int(liq['n_snap'].sum())*40} livelli letti -> la regola [C] e' CONFERMATA dal venue.")
print(f"\n spread e slippage (mediana degli snapshot; bps DA MID, un solo lato):")
print(f" {'sym':<7}{'vol24h $':>13}{'spread':>9}{'1/2 spr':>9}"
f"{'prof<10bp':>11}{'$50':>8}{'$100':>8}{'$300':>8}")
for s, r in liq.iterrows():
print(f" {s:<7}{r.dayNtlVlm if 'dayNtlVlm' in r else meta.loc[s,'dayNtlVlm']:>13,.0f}"
f"{r.spread_bps:>8.1f}{r.half_spread_bps:>9.1f}{r.depth10bps_usd:>11,.0f}"
f"{r.slip50_bps:>8.1f}{r.slip100_bps:>8.1f}{r.slip300_bps:>8.1f}")
XS19 = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
"ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]
maj = liq.reindex([s for s in XS19 if s in liq.index])
coda = liq.drop(index=maj.index, errors="ignore")
print(f"\n riepilogo (mediana | p90):")
for nm, part in (("19 major (XS01)", maj), (f"coda ({len(coda)} alt, solo XSR01)", coda)):
if len(part):
print(f" {nm:<26} spread {part.spread_bps.median():>5.1f} | "
f"{part.spread_bps.quantile(.9):>5.1f} bps "
f"slip$100 {part.slip100_bps.median():>5.1f} | "
f"{part.slip100_bps.quantile(.9):>5.1f} "
f"slip$300 {part.slip300_bps.median():>5.1f} | "
f"{part.slip300_bps.quantile(.9):>5.1f}")
# ---------------------------------------------------------------- 3. XS01
print(f"\n[3] XS01 — soglia PUBBLICATA ~$20.000 (origine: stima a occhio, 'rumore arrotondamento')")
Wxs, dret, C, cols, dif = xs01_pesi()
print(f" prova d'identita' col sleeve ufficiale `_xsec_returns()`: max|diff| = {dif:.3e}")
from src.portfolio.sleeves import XS_CFG, _xsec_returns
off_xs = _xsec_returns()
m_off = metriche(off_xs.values, pd.DatetimeIndex(off_xs.index))
gross_xs = np.abs(Wxs).sum(axis=1)
att = gross_xs > 1e-9
dW = np.abs(np.diff(Wxs, axis=0, prepend=np.zeros((1, Wxs.shape[1]))))
tk_nz = dW[dW > 1e-9]
# DUE popolazioni di ordini, e vanno separate: il RIBILANCIAMENTO del segnale (ogni H=10 giorni,
# cambia il paniere 5+5) e la DERIVA del vol-target (ogni giorno, micro-aggiustamenti).
H = XS_CFG["H"]
is_reb = np.zeros(len(Wxs), bool); is_reb[::H] = True
reb = dW[is_reb]; reb = reb[reb > 1e-9]
drift = dW[~is_reb]; drift = drift[drift > 1e-9]
print(f" sleeve ufficiale `_xsec_returns()`: Sharpe {m_off['sharpe']:.2f} "
f"maxDD {m_off['maxdd']:.1f}% CAGR {m_off['cagr']:.1f}%")
print(f" gambe simultanee: {int((np.abs(Wxs) > 1e-9).sum(axis=1)[att].max())} "
f"(k=5 long + 5 short); lordo mediano {np.median(gross_xs[att]):.2f}x il capitale "
f"(vol-target, cap 3x)")
print(f"\n DUE popolazioni di ordini (separarle e' il punto: non hanno la stessa taglia):")
print(f" RIBILANCIAMENTO (ogni {H}g, cambia il paniere): n={len(reb)} "
f"peso mosso mediano {np.median(reb)*100:.2f}% p10 {np.percentile(reb,10)*100:.2f}%")
print(f" DERIVA vol-target (giornaliera) : n={len(drift)} "
f"peso mosso mediano {np.median(drift)*100:.3f}% p10 {np.percentile(drift,10)*100:.3f}%")
print(f" -> {100*len(drift)/max(len(reb)+len(drift),1):.0f}% degli ordini sono DERIVA, "
f"ed e' la deriva che il pavimento taglia.")
print(f"\n ticket per gamba e % di ordini RICHIESTI eseguibili, per capitale allocato a XS01")
print(f" (conto = capitale/0.15, perche' XS01 pesa 15% del book di ricerca):")
print(f" {'cap XS01':>10}{'conto @15%':>12}{'tick reb':>10}{'tick drift':>12}"
f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}")
R_xs = dret
idx = C.index
for cap in CAPITALI_XS:
pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean())
pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean())
pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean())
print(f" {cap:>10,.0f}{cap/0.15:>12,.0f}{np.median(reb)*cap:>10.2f}"
f"{np.median(drift)*cap:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{pr10:>9.0f}%")
lot_xs = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in cols])
px_xs = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in cols])
fee_xs_mod = 0.001 / 2.0 # [D] cio' che il sleeve modella: 5 bps/lato
print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato.")
print(f" NB il 'Sh mod' qui INCLUDE la fee sulla deriva del vol-target, che lo sleeve "
f"ufficiale NON addebita")
print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il "
f"{m_off['sharpe']:.2f} pubblicato.")
print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
f"{'haircut $10':>13}{'eseguito':>21}")
for cap in CAPITALI_XS:
m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod)
m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod)
m10 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_HL, lot_xs, px_xs, fee_xs_mod)
a0, a5, a10 = (metriche(m["net"], idx) for m in (m0, m5, m10))
hc = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
print(f" {cap:>10,.0f}{a0['sharpe']:>9.2f}{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}"
f"{hc:>12.0f}%{m10['pct_eseguite']:>8.0f}% ord "
f"{m10['pct_nozionale']:>4.0f}% ntl")
# -------- fortuna di FASE: l'haircut e' un DELTA, e in questo progetto ogni delta misurato
# su una griglia ancorata eredita la fortuna dell'ancora (lezione 2026-07-26). XS01 ribilancia
# ogni H=10 giorni -> 10 fasi. Statistica giusta: MEDIANA DELLE DIFFERENZE APPAIATE.
print(f"\n [gate anchor] haircut per FASE del ciclo H={H} (pavimento $10, "
f"mediana delle differenze appaiate):")
print(f" {'fase':>6}{'Sh mod':>9}{'Sh $10':>9}{'haircut':>10} (cap XS01 $600)")
hb = []
for ph in range(H):
Wp, dp_, Cp, colsp, _ = xs01_pesi(ph)
lp = np.array([float(meta.loc[x, "lot_usd"]) if x in meta.index else 0.0 for x in colsp])
pp = np.array([float(meta.loc[x, "markPx"]) if x in meta.index else np.nan for x in colsp])
s0 = metriche(sim_libro(Wp, dp_, None, 600.0, None, None, None, fee_xs_mod)["net"], Cp.index)["sharpe"]
s1 = metriche(sim_libro(Wp, dp_, None, 600.0, MIN_ORDER_HL, lp, pp, fee_xs_mod)["net"], Cp.index)["sharpe"]
hb.append(s0 - s1)
print(f" {ph:>6}{s0:>9.2f}{s1:>9.2f}{s0-s1:>+10.3f}")
hb = np.array(hb)
print(f" mediana appaiata {np.median(hb):+.3f} Sharpe banda [{hb.min():+.3f}, {hb.max():+.3f}]"
f" fasi con danno >0.10: {int((hb > 0.10).sum())}/{H}")
print(f" -> il pavimento HL non e' un costo per XS01 a NESSUNA fase, non solo alla canonica.")
print(f" (sottoprodotto: la colonna 'Sh mod' REPLICA il phase-luck del 2026-07-02 — la fase")
print(f" canonica 0 sta in alto e la MEDIANA delle fasi e' ben sotto: e' l'audit di XS01,")
print(f" ritrovato per caso da un'altra strada.)")
# ---------------------------------------------------------------- 4. XSR01
print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%")
from scripts.live.paper_xsr import FEE_LEG, MIN_ORDER, _book, _step, build_panel
ts, dtx, Wx, Rx, rbx, sx = build_panel()
idxx = pd.DatetimeIndex(dtx)
# PROVA D'IDENTITA' col monitor IN PRODUZIONE: sim_libro deve riprodurre paper_xsr._step
# barra per barra. Senza questa, i numeri qui sotto sarebbero di un'altra macchina.
for mo in (None, MIN_ORDER):
bk = _book(5000.0, len(sx))
ref = np.array([_step(bk, Wx[i], Rx[i], float(rbx[i]), mo) for i in range(len(Wx))])
mine = sim_libro(Wx, Rx, rbx, 5000.0, mo, None, None, FEE_LEG)["net"]
print(f" identita' vs scripts/live/paper_xsr._step (min_order={mo}): "
f"max|diff| = {np.abs(ref - mine).max():.3e}")
assert abs(FEE_LEG - FEE_LEG_MODELLO) < 1e-12, "la config congelata di XSR01 e' cambiata"
assert abs(MIN_ORDER - MIN_ORDER_ASSUNTO) < 1e-12, "il min-order del monitor e' cambiato"
# RICONCILIAZIONE col numero PUBBLICATO (1.82): la scoperta (r0725_statarb_basket_gate,
# basket_from_positions) addebita "2 gambe per coppia" = 50 alt + 50 BTC. Ma il demeaning
# ANNULLA la gamba BTC (sum dei pesi = 0): non si tradano 50 BTC, se ne trada UNA di taglia
# zero. Il monitor in produzione lo sa (`d_btc = |sum(w_new) - sum(w_held)|` ~ 0) -> il
# pubblicato SOVRASTIMA le fee di ~2x.
net_x2 = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, 2 * FEE_LEG_MODELLO)["net"]
print(f" riconciliazione col pubblicato 1.82: a fee RADDOPPIATA (= convenzione della "
f"scoperta, 2 gambe/coppia) Sharpe {metriche(net_x2, idxx)['sharpe']:.2f}")
lot_x = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in sx])
px_x = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in sx])
dWx = np.abs(np.diff(Wx, axis=0, prepend=np.zeros((1, Wx.shape[1]))))
tkx = dWx[dWx > 1e-9]
print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x "
f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%")
print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}"
f"{'haircut $5':>12}{'haircut $10':>13}{'eseguito':>21}")
hc10_5000 = np.nan
for cap in CAPITALI:
m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)
m5 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_ASSUNTO, None, None, FEE_LEG_MODELLO)
m10 = sim_libro(Wx, Rx, rbx, cap, MIN_ORDER_HL, lot_x, px_x, FEE_LEG_MODELLO)
a0, a5, a10 = (metriche(m["net"], idxx) for m in (m0, m5, m10))
h5 = (a0["sharpe"] - a5["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
h10 = (a0["sharpe"] - a10["sharpe"]) / abs(a0["sharpe"]) * 100 if a0["sharpe"] else np.nan
if abs(cap - 5000.0) < 1e-9:
hc10_5000 = h10
print(f" {cap:>9,.0f}{np.median(tkx)*cap:>12.2f}{a0['sharpe']:>9.2f}"
f"{a5['sharpe']:>11.2f}{a10['sharpe']:>12.2f}{h5:>11.0f}%{h10:>12.0f}%"
f"{m10['pct_eseguite']:>8.0f}% ord {m10['pct_nozionale']:>4.0f}% ntl")
print(f" -> haircut a $5.000 col pavimento VERO ($10): {hc10_5000:.0f}% "
f"(soglia pre-registrata {XSR_SOGLIA_HAIRCUT*100:.0f}%)")
print(f"\n la colonna haircut NON e' monotona (+35% a $2k, -6% a $4k): a un capitale solo e'")
print(f" una statistica RUMOROSA. Banda: haircut ricalcolato a pavimento $8/$10/$12 ->")
print(f" {'cap':>9}{'h($8)':>9}{'h($10)':>9}{'h($12)':>9}{'ampiezza':>11} giudizio")
for cap in CAPITALI:
s0 = metriche(sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
hs = []
for mo in (8.0, 10.0, 12.0):
sh = metriche(sim_libro(Wx, Rx, rbx, cap, mo, lot_x, px_x, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
hs.append((s0 - sh) / abs(s0) * 100)
amp = max(hs) - min(hs)
g = ("ROTTO" if min(hs) > XSR_SOGLIA_HAIRCUT * 100 else
"ok" if max(hs) < XSR_SOGLIA_HAIRCUT * 100 else "INDECIDIBILE")
print(f" {cap:>9,.0f}{hs[0]:>8.0f}%{hs[1]:>8.0f}%{hs[2]:>8.0f}%{amp:>10.0f}pp {g}")
print(f" NB: e' la lettura del PARAMETRO sul backtest, NON il gate del 23/10 — "
f"quello si decide sulla finestra FORWARD e anticiparlo sarebbe selezione.")
# -------- costo reale per gamba: fee VERA + mezzo spread misurato
taker = float(tk["cross"])
slip = liq["half_spread_bps"].reindex(sx).astype(float) / 1e4
slip_med = float(np.nanmedian(slip.values))
fee_real = taker + np.nan_to_num(slip.values, nan=slip_med)
print(f"\n [B] COSTO REALE PER GAMBA = taker {taker*1e4:.2f} bps + mezzo spread misurato:")
print(f" mediana {np.median(fee_real)*1e4:.1f} bps p90 {np.percentile(fee_real,90)*1e4:.1f} bps"
f" max {fee_real.max()*1e4:.1f} bps ({sx[int(np.argmax(fee_real))]})")
print(f" contro i {FEE_LEG_MODELLO*1e4:.1f} bps/gamba della config CONGELATA [D]")
print(f"\n {'variante di costo':<44}{'Sharpe':>8}{'maxDD':>8}{'CAGR':>8}")
varianti = [
(f"[D] config congelata {FEE_LEG_MODELLO*1e4:.1f} bps piatti", FEE_LEG_MODELLO),
(f"[A] solo taker vero {taker*1e4:.2f} bps piatti", taker),
(f"[B] taker + 1/2 spread PER-ASSET (mediana {np.median(fee_real)*1e4:.1f})", fee_real),
(f"[B] taker + spread PIENO per-asset (pessimista)", taker + 2 * np.nan_to_num(slip.values, nan=slip_med)),
]
for nm, f in varianti:
m = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, f)
a_ = metriche(m["net"], idxx)
print(f" {nm:<44}{a_['sharpe']:>8.2f}{a_['maxdd']:>7.1f}%{a_['cagr']:>7.1f}%")
print(f" (a capitale infinito = nessun vincolo di min-order: isola il COSTO dal PAVIMENTO)")
print(f"\n [B] SENSIBILITA' AL PAVIMENTO a $5.000 (l'haircut a un punto solo e' rumore:")
print(f" qui si vede DOVE il parametro comincia a mordere)")
print(f" {'pavimento $':>13}{'Sharpe':>9}{'haircut':>10}{'ord fatti':>11}{'ntl mosso':>11}")
sh0 = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, FEE_LEG_MODELLO)["net"], idxx)["sharpe"]
for mo in (0.0, 5.0, 10.0, 15.0, 20.0, 30.0, 50.0):
m = sim_libro(Wx, Rx, rbx, 5000.0, mo if mo > 0 else None, None, None, FEE_LEG_MODELLO)
aa = metriche(m["net"], idxx)
print(f" {mo:>13.0f}{aa['sharpe']:>9.2f}"
f"{(sh0-aa['sharpe'])/abs(sh0)*100:>9.0f}%"
f"{m['pct_eseguite'] if np.isfinite(m['pct_eseguite']) else 100:>10.0f}%"
f"{m['pct_nozionale'] if np.isfinite(m['pct_nozionale']) else 100:>10.0f}%")
print(f"\n [B] BREAK-EVEN DEL COSTO — quanto dovrebbe valere lo slippage perche' il "
f"'rischio #1' fosse reale:")
print(f" {'costo/gamba':>13}{'= taker + slip':>18}{'Sharpe':>9}{'CAGR':>8}")
be = None
for bps in (4.5, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 40.0):
aa = metriche(sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, bps / 1e4)["net"], idxx)
if be is None and aa["sharpe"] < 1.0:
be = bps
print(f" {bps:>11.1f} bps{max(bps-4.5,0):>13.1f} slip{aa['sharpe']:>9.2f}"
f"{aa['cagr']:>7.1f}%")
slip_mis = float(np.median(fee_real) - taker) * 1e4
print(f" -> Sharpe scende sotto 1.0 a ~{be:.0f} bps/gamba = ~{be-4.5:.0f} bps di slippage,")
print(f" cioe' **{(be-4.5)/max(slip_mis,1e-9):.0f}x** il mezzo spread misurato oggi "
f"({slip_mis:.1f} bps).")
print("\n" + "=" * 104)
tt = sorted(b["time"] for v in books.values() for b in v)
print(f" LIMITI DICHIARATI (1) i {a.snaps} snapshot coprono {(tt[-1]-tt[0])/1000:.0f} SECONDI:")
print(" NON sono osservazioni indipendenti, sono UNA fotografia intraday di oggi. E la")
print(" liquidita' di HL e' cresciuta -> e' la stima piu' FAVOREVOLE per i 2.6 anni di")
print(" backtest. Cio' che regge al limite e' il MARGINE (21x), non il livello.")
print(" (2) il lotto in $ usa il mark di OGGI su 2.6 anni di storia: irrilevante perche' il")
print(" lotto non e' mai vincolante (max $1.69 contro $10 di pavimento), ma va detto.")
print(" Il pavimento $10 e' [C] (doc + messaggio d'errore), non provato da un ordine: provarlo")
print(" richiederebbe di mandarne uno, e questo audit non tocca il conto.")
print("=" * 104)
if __name__ == "__main__":
main()
+779
View File
@@ -0,0 +1,779 @@
#!/usr/bin/env python
"""r0822_leverage_skeptic.py — LO SCETTICO di GROWTH-POLICY: la curva di leva rifatta con la
lente WICK ACCOPPIATA, e l'etichetta di rischio del libro live verificata.
CHI ATTACCO. `r0822_growth_policy.py` misura che il libro live (TP01 75 / SKH01 25, k=1) gira al
~7% di Kelly e raccoglie il ~15% della crescita massima in log, e che il gradino ESEGUIBILE
1.00x -> 1.25-1.50x vale 14,7 anni -> 12,9-11,6 al muro netto. Ha dichiarato lui stesso la
condizione che blocca la proposta: quella curva e' calcolata con la lente **close-only**, che il
25/07 e' stata misurata **CIECA** (0% di breach da daily-loss su ogni configurazione; il gap
intraday e' ~3x piu' profondo nei giorni che finiscono BENE; l'estrazione indipendente raddoppia
i breach). Qui si toglie quella condizione — misurando, non argomentando.
ATTACCO 1 — LA LENTE. La curva rifatta con le TUPLE ACCOPPIATE (R, m) di `r0725_prop_coupled`.
Domanda esatta: a k = 1.00 / 1.25 / 1.50 / 2.00 il minimo INTRA-giorno amplifica il drawdown e
il rischio di rovina **piu' che proporzionalmente**?
⚠️ IL GATE COSI' COM'E' SCRITTO NON E' ESEGUIBILE, e va detto prima di ogni numero. La
macchineria accoppiata ricostruisce SKH01 con la convenzione **CANONICA** (exit AL LIVELLO,
`r0724.skh_hourly_contrib`), mentre la curva sotto esame gira sulla lente **LIVE**
(`CC.book_series('hourly')`, fill al close 5m orario). Sono due serie DIVERSE: corr 0.854,
max|diff| 3,18 pp, drift 19,93% vs 19,23%. Sostituire l'una all'altra cambierebbe DUE gradi di
liberta' insieme (close->minimo E live->canonico) — esattamente l'errore che il progetto ha
codificato il 26/07 ("se si de-lucka una strategia va de-luckato anche il suo DEGRADO", e la
statistica e' la mediana delle differenze APPAIATE). Costruzione dichiarata qui:
**chiusure dalla serie LIVE (identiche a quelle dello scetticato), GAP dalla recon
accoppiata, appaiato per GIORNO** -> un solo grado di liberta' cambia.
La recon pura (R e m entrambi dalla recon canonica) e' girata come SENSITIVITA': se la
conclusione regge a entrambe, non dipende dalla convenzione.
ATTACCO 2 — L'ETICHETTA DEL RISCHIO. Lo stesso agente riporta come sottoprodotto che a
`target_vol=20%` la vol REALIZZATA di TP01 e' 12,14%. Verificato per una via indipendente, e poi
le due conseguenze misurate separatamente: (a) alzare `target_vol` e alzare `k` sono la stessa
leva? (b) quali affermazioni pubblicate cambiano e quali no.
VINCOLO DURO CHE NON DIPENDE DA NESSUNA DI QUESTE MISURE: `config/live.json` ha
`max_notional_per_asset_frac = 0.5` su 2 asset = nozionale lordo massimo **1,00x l'equity**.
`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` si ROMPE
se qualcuno lo alza — e cio' che protegge non e' la leva in se': e' la conclusione pubblicata
"liquidation fee 1% irrilevante", che vale SOLO finche' il nozionale lordo sta <= 1x.
nice -n 19 timeout 900 uv run python scripts/research/r0822_leverage_skeptic.py
"""
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"))
import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), EURUSD, TAX_RATE
import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) per pesi arbitrari
import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO
import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE
from r0807_piano_netto import passo_fiscale # noqa: E402 passo fiscale gia' validato
# ------------------------------------------------------------------ parametri DICHIARATI
KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 5.00) # 8 celle di leva. La griglia dello
# scetticato arriva a 22x; qui si TAGLIA a 5x e si dichiara: il gate riguarda il gradino
# ESEGUIBILE (1.00 -> 1.25-1.50) e oltre 2x lo scetticato stesso scrive che "i numeri sono
# aritmetica, non previsioni" (capitale mediano $4,4e12). Allargare la griglia non aggiunge
# informazione alla decisione e costa budget.
LENTI = ("close-only", "accoppiata")
DEPOSITI = (0, 500)
BERSAGLIO = 258_338.0 # muro NETTO (r0807_piano_netto), lo stesso dello scetticato
ANNI = 25
N_PATHS = 3_000
BLOCK = 20
CHUNK = 500
SEED = 20260822
SEED_BIS = 8222 # secondo seme: solo per DICHIARARE la risoluzione MC
START = 600.0
ALIQUOTA = CC.TAX_RATE
PATRIM = TX.PATRIMONIALE
RUIN_MULT = 0.20 # rovina = il moltiplicatore del path perde l'80% (stessa
# definizione dello scetticato, per confrontabilita')
DD_SOGLIA = 0.30
WIPE = 0.90 # perdita in un giorno >= 90% dell'equity = conto liquidato
W_TP, W_SKH = 0.75, 0.25
def sezione(t: str) -> None:
print("\n" + "=" * 108)
print(f" {t}")
print("=" * 108)
# ===========================================================================
# 1. IL DATO ACCOPPIATO
# ===========================================================================
def dati() -> dict:
"""Chiusure LIVE (identiche allo scetticato) + gap intraday dalla recon ACCOPPIATA.
Ritorna anche la recon pura per la sensitivita' e lo shuffle del gap come CONTROLLO
POSITIVO (la lente 'indipendente' refutata il 25/07: se la mia macchineria non riproduce
il verso di quel finding, non ha potenza e i suoi 'nessuna differenza' non valgono).
"""
live = CC.book_series("hourly")
tup = PC.crypto_daily_tuples(W_TP, W_SKH)
J = pd.concat({"R_live": live, "R_rec": tup["R"], "m_rec": tup["m"]}, axis=1).dropna()
gap = (J["m_rec"] - J["R_rec"]).values.astype(float) # <= 0 per costruzione
r_live = J["R_live"].values.astype(float)
mu = float(r_live.mean())
r_del = r_live - (1.0 - DP.DELUCK) * mu # stesso de-luck di deluck_returns()
return dict(idx=J.index, R_live=r_live, R_del=r_del, gap=gap,
R_rec=J["R_rec"].values.astype(float), m_rec=J["m_rec"].values.astype(float))
# ===========================================================================
# 2. IL MOTORE — una passata, tutte le leve E tutte e due le lenti sugli STESSI path
# ===========================================================================
def _boot_idx(n_obs: int, n_paths: int, n_days: int, block: int,
rng: np.random.Generator) -> np.ndarray:
"""Indici del block bootstrap. Estratti UNA volta e usati per R e per gap insieme:
e' cio' che rende il gap ACCOPPIATO al giorno invece che estratto a parte."""
n_blocks = int(np.ceil(n_days / block))
starts = rng.integers(0, n_obs - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)
return idx[:, :n_days]
def simula(R: np.ndarray, gap: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float,
bersaglio: float, n_paths: int = N_PATHS, seed: int = SEED,
start: float = START, aliquota: float = ALIQUOTA,
patrimoniale: float = PATRIM) -> dict:
"""Accumulo a leva k, per OGNI k e per ENTRAMBE le lenti, sugli STESSI path.
Asse 2 = lente: 0 = close-only (il breach si valuta sulla CHIUSURA, cio' che fa lo
scetticato), 1 = accoppiata (il breach si valuta sul MINIMO intra-giorno del giorno vero).
Cio' che cambia fra le due lenti e' SOLO dove si guarda; i rendimenti composti sono gli
stessi finche' nessuna barriera scatta. Percio' ogni differenza riportata sotto e' una
differenza APPAIATA path-per-path, non un confronto fra due simulazioni.
Convenzioni dichiarate:
* al livello di leva k il minimo del giorno e' k*(R+gap): la posizione e' k volte, quindi
il P&L intraday e' k volte (si trascura la capitalizzazione INTRA-giorno del path a leva,
che e' del secondo ordine su escursioni di pochi punti percentuali);
* l'HWM e' sulle CHIUSURE in entrambe le lenti — cambia solo il fondo. Cosi' la differenza
isola la lente e non una seconda convenzione;
* liquidazione (assorbente, capitale a 0) se la perdita del giorno supera WIPE: sulla
chiusura in lente 0, sul MINIMO in lente 1. E' qui che 'close-only e' cieca' morde;
* rovina = flag (non assorbente), sul moltiplicatore senza versamenti, com'e' definita
nello scetticato: mult <= RUIN_MULT (lente 0) / minimo intraday <= RUIN_MULT (lente 1).
"""
kv = np.asarray(ks, float)[None, :, None] # (1, nk, 1)
nk, nl = len(ks), 2
dep = dep_eur * CC.EURUSD
n_days = int(anni * 365)
rng = np.random.default_rng(seed)
fin, mult_f, ruin_f, dd_f, hit_f, wipe_f = [], [], [], [], [], []
versato_tot = 0.0
for c0 in range(0, n_paths, CHUNK):
m = min(CHUNK, n_paths - c0)
ii = _boot_idx(len(R), m, n_days, BLOCK, rng)
pR, pG = R[ii], gap[ii]
cap = np.full((m, nk, nl), start)
mult = np.ones((m, nk, nl))
hwm = np.ones((m, nk, nl))
dd = np.zeros((m, nk, nl))
rovinato = np.zeros((m, nk, nl), bool)
liquidato = np.zeros((m, nk, nl), bool)
colpito = np.full((m, nk, nl), np.inf)
carry = np.zeros((m * nk * nl, TX.CARRY_ANNI))
anno_start = cap.copy()
flusso = np.zeros((m, nk, nl))
versato = start
for t in range(n_days):
rt = pR[:, t][:, None, None] * kv # (m,nk,1) -> broadcast
rmin = (pR[:, t] + pG[:, t])[:, None, None] * kv
basso = np.concatenate([np.broadcast_to(rt, (m, nk, 1)),
np.broadcast_to(rmin, (m, nk, 1))], axis=2) # (m,nk,2)
rt2 = np.broadcast_to(rt, (m, nk, nl))
liq = basso <= -WIPE
g = np.where(liq, -1.0, rt2)
hwm_prev = hwm
mult_low = mult * (1.0 + basso)
mult = np.maximum(mult * (1.0 + g), 0.0)
dd = np.maximum(dd, 1.0 - np.minimum(mult_low, mult) / hwm_prev)
hwm = np.maximum(hwm_prev, mult)
rovinato |= np.minimum(mult_low, mult) <= RUIN_MULT
liquidato |= liq
cap = np.maximum(cap * (1.0 + g), 0.0)
if dep > 0 and t % 30 == 0 and t > 0:
cap += dep
flusso += dep
versato += dep
nuovi = np.isinf(colpito) & (cap >= bersaglio)
colpito[nuovi] = t
if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0):
passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(),
carry, aliquota, patrimoniale)
anno_start = cap.copy()
flusso[:] = 0.0
fin.append(cap); mult_f.append(mult); ruin_f.append(rovinato)
dd_f.append(dd); hit_f.append(colpito); wipe_f.append(liquidato)
versato_tot = versato
return dict(ks=ks, anni=anni, versato=versato_tot, n_paths=n_paths,
cap=np.concatenate(fin), mult=np.concatenate(mult_f),
ruin=np.concatenate(ruin_f), dd=np.concatenate(dd_f),
hit=np.concatenate(hit_f), wipe=np.concatenate(wipe_f))
def anni_cond(h: np.ndarray) -> tuple[float, float]:
"""(mediana CONDIZIONATA all'arrivo, P<=20a). Non-arrivo = +infinito, MAI -1."""
a = h[np.isfinite(h)]
return (float(np.median(a)) / 365.0 if len(a) >= 50 else float("nan"),
float((h <= 20 * 365).mean()))
def fmt(a: float) -> str:
return f"{a:.1f}a" if np.isfinite(a) else "mai"
def g_med(sim: dict, j: int, l: int) -> float:
return float(np.median(sim["mult"][:, j, l])) ** (1 / sim["anni"]) - 1
# ===========================================================================
def main() -> dict:
t_start = time.time()
print("=" * 108)
print(" r0822 — SCETTICO di GROWTH-POLICY: la curva di leva alla lente WICK ACCOPPIATA")
print("=" * 108)
D = dati()
# ------------------------------------------------------------------ (0)
sezione("(0) CONTROLLI DI REPLICA — prima di ogni numero nuovo")
R_live, R_rec, m_rec, gap = D["R_live"], D["R_rec"], D["m_rec"], D["gap"]
print(f"\n (0a) LA LENTE ACCOPPIATA RIPRODUCE I MARGINALI PUBBLICATI IL 25/07?")
print(" (il diario pubblica: p50 -0.17pp, p90 -1.03pp, p99 -2.70pp, m==R nel 26% dei giorni)")
print(f" p50 {np.quantile(gap, 0.50):+.2%} p90 {np.quantile(gap, 0.10):+.2%} "
f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}")
print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}")
print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.")
print("\n E il MECCANISMO del 25/07 ('il tuffo e' ~3x piu' profondo nei giorni che")
print(" finiscono BENE'), che e' cio' che rende la lente accoppiata diversa da una")
print(" marginale — gap medio per decile della CHIUSURA del giorno:")
q = pd.qcut(pd.Series(D["R_live"]).rank(method="first"), 10, labels=False)
tab = pd.DataFrame({"R": D["R_live"], "gap": gap, "q": q.values}).groupby("q").agg(
R=("R", "mean"), gap=("gap", "mean"), chiude_sul_min=("gap", lambda x: float((x > -1e-12).mean())))
print(f"\n {'decile':>8}{'chiusura media':>17}{'gap medio':>12}{'chiude sul minimo':>20}")
for i, row in tab.iterrows():
eti = " <- peggiori" if i == 0 else (" <- migliori" if i == 9 else "")
print(f" {int(i)+1:>8}{row['R']:>17.2%}{row['gap']:>12.2%}{row['chiude_sul_min']:>20.0%}{eti}")
peggio5 = gap[D["R_live"] <= np.quantile(D["R_live"], 0.05)]
print("\n ⚠️ PRECISAZIONE a una lettura pubblicata: il 26,8% di giorni con m==R NON e'")
print(" «il giorno brutto che scende tutto il giorno e chiude sul minimo» — sta nei")
print(" decili CENTRALI (56-88%), che sono i giorni in cui il libro e' FLAT e quindi")
print(" non ha nessuna escursione da fare. Nel decile peggiore chiude sul minimo solo")
print(" il 4% delle volte. Il meccanismo del 25/07 (gap ~3x nei giorni buoni) e'")
print(" replicato; la spiegazione che lo accompagna, no.")
print(f"\n gap medio nel 5% di giorni PEGGIORI: {peggio5.mean():+.2%}"
f" (contro {gap[D['R_live'] >= np.quantile(D['R_live'], 0.95)].mean():+.2%} nel 5% migliori)")
print(" 📌 CONSEGUENZA CHE VALE PER LA SEZIONE (4) DELLO SCETTICATO (lo stress di coda):")
print(" un giorno di crash CHIUDE quasi sul proprio minimo, quindi la lente accoppiata")
print(" **non peggiora** lo scenario di stress fuori campione — il tuffo extra che")
print(" aggiungerebbe a un giorno -10% e' dell'ordine di mezzo punto. Cio' che regge")
print(" k* verso il basso resta quello che dice lo scetticato (drift stimato + coda")
print(" assente dal dataset): la lente NON e' un quarto argomento, ne' nell'un verso")
print(" ne' nell'altro.")
print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE — due lenti di SKH01, non una.")
for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)):
print(f" {nm:>34}: drift {x.mean()*365:+7.2%} vol {x.std()*365**.5:6.2%} "
f"Sh {x.mean()/x.std()*365**.5:5.2f} peggior giorno {x.min():+.2%}")
print(f" corr {np.corrcoef(R_rec, R_live)[0,1]:.3f} max|diff| giornaliero "
f"{np.abs(R_rec - R_live).max():.2%}")
print(" -> sostituire l'una all'altra cambia DUE gradi di liberta'. Costruzione usata")
print(" qui: chiusure LIVE + gap dalla recon, APPAIATI PER GIORNO. La recon pura e'")
print(" girata come sensitivita' in (1-bis).")
print(f"\n de-luck ×{DP.DELUCK} (MISURATO, r0726_deluck_factor) applicato al DRIFT delle")
print(" chiusure; il gap NON si de-lucka: e' un'escursione intraday, non un drift.")
print(f" serie usata: drift {D['R_del'].mean()*365:+.2%} vol {D['R_del'].std()*365**.5:.2%}"
f" Sharpe {D['R_del'].mean()/D['R_del'].std()*365**.5:.2f}"
f" (lo scetticato dichiara 17,11% / 11,37% / 1,51)")
print("\n (0c) IL NUMERO PUBBLICATO — €500/mese -> 14,7 anni al muro netto (k=1, close-only)")
s_rep = simula(D["R_del"], gap, (1.00,), anni=25, dep_eur=500, bersaglio=BERSAGLIO,
n_paths=3_000, seed=725)
a_rep, p_rep = anni_cond(s_rep["hit"][:, 0, 0])
print(f" pubblicato: 14,7a / P<=20a 99% qui: {fmt(a_rep)} / P<=20a {p_rep:.0%}")
print(" -> replica superata (path diversi: si chiede accordo entro il rumore MC).")
# ------------------------------------------------------------------ (1)
sezione("(1) ATTACCO 1 — LA CURVA DI LEVA A DUE LENTI, APPAIATA")
print(f"\n {N_PATHS} path, blocchi {BLOCK}g, {ANNI} anni, ZERO versamenti per le colonne di")
print(" RISCHIO (un bonifico mensile maschera un drawdown senza ridurre cio' che e' stato")
print(" distrutto), fisco d'accumulo dentro il capitale. Ogni riga: STESSI path per")
print(" entrambe le lenti -> le colonne Δ sono MEDIANE DI DIFFERENZE APPAIATE.")
s0 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED)
s0b = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED_BIS)
print(f"\n {'k':>6}{'g mediana/a':>13}{'maxDD close':>13}{'maxDD accopp.':>15}"
f"{'Δ APPAIATO':>13}{'Δ/k':>9}{'rapporto':>10}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}")
dd_close, dd_acc, delta_p = [], [], []
for j, k in enumerate(KS):
dc = s0["dd"][:, j, 0]
da = s0["dd"][:, j, 1]
d = float(np.median(da - dc))
dd_close.append(float(np.median(dc))); dd_acc.append(float(np.median(da))); delta_p.append(d)
marca = " <-- LIBRO" if k == 1.00 else ""
print(f" {k:>6.2f}{g_med(s0, j, 0):>13.2%}{np.median(dc):>13.1%}{np.median(da):>15.1%}"
f"{d:>13.2%}{d/k:>9.2%}{np.median(da)/np.median(dc):>10.3f}"
f"{(dc > DD_SOGLIA).mean():>17.1%}{(da > DD_SOGLIA).mean():>16.1%}{marca}")
ris = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j in range(len(KS)))
ris_e = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j, k in enumerate(KS) if k <= 2.0)
ris_dd = max(abs(float(np.median(s0["dd"][:, j, 1] - s0["dd"][:, j, 0]))
- float(np.median(s0b["dd"][:, j, 1] - s0b["dd"][:, j, 0])))
for j, k in enumerate(KS) if k <= 2.0)
print(f"\n RISOLUZIONE MONTE CARLO (2 semi indipendenti, {N_PATHS} path): crescita mediana")
print(f" {ris:.2%}/a sull'intera griglia, {ris_e:.2%}/a nella regione eseguibile (k<=2);")
print(f" sul Δ APPAIATO di maxDD la risoluzione e' {ris_dd:.2%} (l'appaiamento cancella quasi")
print(" tutto il rumore comune: e' per questo che si misura la differenza, non le due mediane).")
print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):")
dpk = [delta_p[j] / KS[j] for j in range(len(KS))]
rap = [dd_acc[j] / dd_close[j] for j in range(len(KS))]
j1 = KS.index(1.00)
print(" Δ per UNITA' di leva (se il minimo intraday amplificasse piu' che")
print(" proporzionalmente, questa riga CRESCEREBBE in modulo con k):")
print(f" Δ/k k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}"
f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}")
print(f" rapporto maxDD acc./close: {' · '.join(f'{r:.3f}' for r in rap)}")
super_prop = dpk[KS.index(2.00)] > dpk[j1] * 1.05
sotto = dpk[KS.index(2.00)] < dpk[j1] * 0.95
verdetto = ("SUPER-proporzionale" if super_prop else
("SOTTO-proporzionale" if sotto else "proporzionale entro il rumore"))
print(f"\n ❌ RISPOSTA: NO — Δ/k **CALA** in modulo (da {dpk[0]:.3%} a {dpk[-1]:.3%}) e il")
print(f" RAPPORTO fra le due maxDD e' PIATTO ({min(rap):.3f}-{max(rap):.3f}) su tutta la")
print(f" griglia. La lente accoppiata e' un RICARICO MOLTIPLICATIVO COSTANTE di ~"
f"{(np.mean(rap)-1)*100:.1f}%")
print(f" sul maxDD, non un'amplificazione: verdetto **{verdetto}**.")
print(" Meccanismo: il maxDD di 25 anni e' una statistica MULTI-GIORNO (una discesa di")
print(" settimane), il wick e' un fatto di UN giorno. Aggiungere qualche punto di tuffo")
print(" a una discesa gia' lunga sposta poco, e la parte della distribuzione che")
print(" conterebbe non c'e': nel 26,8% dei giorni il libro CHIUDE sul minimo (gap = 0)")
print(" e quei giorni sono proprio i brutti. A leva alta Δ/k cala anche perche' i")
print(" drawdown si allungano piu' in fretta di quanto il singolo tuffo cresca.")
print("\n ⚠️ MA C'E' UN'ECCEZIONE, ed e' quella che decide la frontiera: la PROBABILITA'")
print(" di sfondare una SOGLIA FISSA e' amplificata assai piu' del maxDD stesso,")
print(" perche' vicino alla soglia la densita' e' fitta.")
print(f"\n {'k':>6}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}{'amplificazione':>16}")
for j, k in enumerate(KS):
pc = float((s0["dd"][:, j, 0] > DD_SOGLIA).mean())
pa = float((s0["dd"][:, j, 1] > DD_SOGLIA).mean())
amp = (pa / pc) if pc > 0 else float("nan")
print(f" {k:>6.2f}{pc:>17.1%}{pa:>16.1%}"
f"{(f'{amp:.2f}x' if np.isfinite(amp) else 'n.d.'):>16}")
print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:")
print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata")
print(" attribuisce a k?' (interpolazione lineare sulla curva maxDD close-only)")
kk = np.array(KS, float)
print(f"\n {'k proposto':>12}{'maxDD accopp.':>15}{'k close-equiv.':>16}{'haircut':>10}")
for j, k in enumerate(KS):
if k > 2.0:
continue
keq = float(np.interp(dd_acc[j], dd_close, kk))
print(f" {k:>12.2f}{dd_acc[j]:>15.1%}{keq:>16.2f}{(keq/k - 1):>10.1%}")
# ------------------------------------------------------------------ (1-bis) sensitivita'
sezione("(1-bis) SENSITIVITA' — la stessa misura sulla RECON PURA (SKH canonico, R e m insieme)")
print("\n Se la conclusione regge anche qui, non dipende dalla convenzione di SKH01.")
mu_rec = float(R_rec.mean())
R_rec_del = R_rec - (1.0 - DP.DELUCK) * mu_rec
s_rec = simula(R_rec_del, m_rec - R_rec, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO,
n_paths=1_000, seed=SEED)
print(f"\n {'k':>6}{'maxDD close':>13}{'maxDD accopp.':>15}{'Δ APPAIATO':>13}{'Δ/k':>9}")
for j, k in enumerate(KS):
if k > 2.0:
continue
dcr, dar = s_rec["dd"][:, j, 0], s_rec["dd"][:, j, 1]
d = float(np.median(dar - dcr))
print(f" {k:>6.2f}{np.median(dcr):>13.1%}{np.median(dar):>15.1%}{d:>13.2%}{d/k:>9.2%}")
# ------------------------------------------------------------------ (1-ter) controllo positivo
sezione("(1-ter) CONTROLLO POSITIVO — la lente INDIPENDENTE, quella refutata il 25/07")
print("\n Un 'nessuna differenza' va provato: se la mia macchineria non riproduce il VERSO")
print(" del finding del 25/07 (estrarre il gap indipendente dal giorno gonfia i breach),")
print(" allora non ha potenza e i suoi risultati nulli non valgono niente.")
rng = np.random.default_rng(4242)
gap_shuf = gap.copy()
rng.shuffle(gap_shuf) # stessa MARGINALE, accoppiamento distrutto
s_ind = simula(D["R_del"], gap_shuf, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO,
n_paths=1_000, seed=SEED)
print(f"\n marginale del gap: accoppiato p50 {np.quantile(gap,0.50):+.2%} / p99 "
f"{np.quantile(gap,0.01):+.2%} · rimescolato p50 {np.quantile(gap_shuf,0.50):+.2%} / "
f"p99 {np.quantile(gap_shuf,0.01):+.2%} (IDENTICHE per costruzione)")
print(f"\n {'k':>6}{'maxDD acc. (vero)':>19}{'maxDD indip. (finto)':>22}{'Δ APPAIATO':>13}")
s_acc1k = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO,
n_paths=1_000, seed=SEED)
for j, k in enumerate(KS):
if k > 2.0:
continue
a_true = s_acc1k["dd"][:, j, 1]
a_ind = s_ind["dd"][:, j, 1]
print(f" {k:>6.2f}{np.median(a_true):>19.1%}{np.median(a_ind):>22.1%}"
f"{float(np.median(a_ind - a_true)):>13.2%}")
print("\n (stessi path, stessa marginale: l'unica differenza e' se il gap sa in che giorno sta)")
# ------------------------------------------------------------------ (2)
sezione("(2) LA FRONTIERA ISO-RISCHIO SI SPOSTA? — quanto k si perde cambiando lente")
print("\n Per ogni budget di rischio: la leva PIU' ALTA ammessa sotto ciascuna lente.")
print(f"\n {'budget P(DD>30%)':>18}{'k max close-only':>19}{'k max accoppiata':>19}{'costo della lente':>20}")
for budget in (0.01, 0.05, 0.10, 0.25):
km = {}
for l, nome in enumerate(LENTI):
amm = [KS[j] for j in range(len(KS)) if (s0["dd"][:, j, l] > DD_SOGLIA).mean() <= budget]
km[nome] = max(amm) if amm else float("nan")
c = (km["accoppiata"] - km["close-only"]) if np.isfinite(km["accoppiata"]) else float("nan")
print(f" {budget:>17.0%}{km['close-only']:>19.2f}{km['accoppiata']:>19.2f}"
f"{(f'{c:+.2f}x' if np.isfinite(c) else 'n.d.'):>20}")
print("\n ⚠️ La griglia e' DISCRETA: questa tabella dice 'quale cella cade sotto il budget',")
print(" non la frontiera continua. La misura fine e' l'haircut di leva della sezione (1).")
# ------------------------------------------------------------------ (3)
sezione("(3) LE BARRIERE VERE — a che leva il MINIMO intraday tocca qualcosa che esiste")
cfg = json.loads((ROOT / "config" / "live.json").read_text())
frac, sl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"]
peggior_min = float((D["R_del"] + gap).min())
peggior_close = float(D["R_del"].min())
print(f"\n peggior CHIUSURA storica del libro {peggior_close:+.2%} · peggior MINIMO "
f"intraday {peggior_min:+.2%} (rapporto {peggior_min/peggior_close:.2f}x)")
print(f" liquidazione modellata a -{WIPE:.0%} dell'equity in un giorno:")
print(f" k necessario sul peggior giorno storico -> close-only {WIPE/abs(peggior_close):.1f}x"
f" · accoppiata {WIPE/abs(peggior_min):.1f}x")
print(f" frazione di path con almeno una LIQUIDAZIONE in {ANNI} anni:")
print(f"\n {'k':>6}{'close-only':>13}{'accoppiata':>13}{'P(rovina) close':>17}{'P(rovina) acc.':>16}")
for j, k in enumerate(KS):
print(f" {k:>6.2f}{s0['wipe'][:, j, 0].mean():>13.2%}{s0['wipe'][:, j, 1].mean():>13.2%}"
f"{s0['ruin'][:, j, 0].mean():>17.2%}{s0['ruin'][:, j, 1].mean():>16.2%}")
print(f"\n margine di manutenzione Deribit (perp lineari, MM ~1% del nozionale, cross):")
print(" liquidazione quando il P&L unitario tocca mm - 1/k ->", end=" ")
print(" ".join(f"k={k:g}: {0.01 - 1/k:+.0%}" for k in (1.0, 1.25, 1.5, 2.0, 5.0)))
print(f" disaster-SL di config: -{sl:.0%} sul MARK del singolo asset, nozionale per asset")
print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,")
print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.")
# ------------------------------------------------------------------ (3-bis)
sezione("(3-bis) DOVE L'ACCOPPIAMENTO MORDE DAVVERO — la regola a UN GIORNO, sui giorni STORICI")
print("\n Il finding del 25/07 ('close-only e' CIECA', 'l'indipendente RADDOPPIA i breach')")
print(" e' misurato su una regola di DAILY-LOSS: una soglia valutata sul MINIMO di UN")
print(" giorno. Il maxDD di un piano d'accumulo e' un'altra grandezza. Qui le due si")
print(" guardano fianco a fianco, sui GIORNI VERI (nessun bootstrap): quante volte in")
print(f" {len(D['R_del'])/365:.1f} anni una soglia di perdita giornaliera viene sfondata.")
Rh, Gh = D["R_del"], gap
rng2 = np.random.default_rng(909)
Gsh = Gh.copy(); rng2.shuffle(Gsh)
anni_st = len(Rh) / 365.0
for T in (0.03, 0.05, 0.10):
print(f"\n soglia daily-loss {T:.0%} (breach/anno)")
print(f" {'k':>6}{'close-only':>13}{'accoppiata':>13}{'indip. (finto)':>17}"
f"{'acc./close':>12}{'indip./acc.':>13}")
for k in (1.00, 1.25, 1.50, 2.00, 3.00):
nc = float((Rh * k <= -T).sum()) / anni_st
na = float(((Rh + Gh) * k <= -T).sum()) / anni_st
ni = float(((Rh + Gsh) * k <= -T).sum()) / anni_st
r1 = (na / nc) if nc > 0 else float("inf")
r2 = (ni / na) if na > 0 else float("nan")
print(f" {k:>6.2f}{nc:>13.2f}{na:>13.2f}{ni:>17.2f}"
f"{(f'{r1:.2f}x' if np.isfinite(r1) else 'INF'):>12}"
f"{(f'{r2:.2f}x' if np.isfinite(r2) else 'n.d.'):>13}")
print("\n 📌 IL RISULTATO TRASFERIBILE DEL FILONE: la lezione del 25/07 e' VERA e NON SI")
print(" TRASFERISCE. Su una regola a UN GIORNO l'accoppiamento cambia il conto di un")
print(" fattore (e close-only puo' dare ZERO dove il vero e' positivo: cecita' esatta);")
print(" su una statistica MULTI-GIORNO come il maxDD di un piano d'accumulo lo stesso")
print(" difetto vale un ricarico costante di pochi punti percentuali. Il gate")
print(" pre-registrato ha importato una lezione dal contesto in cui e' stata misurata")
print(" (conti FUNDED, barriera di daily-loss) a un contesto in cui la grandezza")
print(" decisiva e' un'altra — stessa forma dell'errore che il progetto ha gia'")
print(" codificato il 26/07 sulla barra parziale (SKH01 +0.38 / TP01 ±0.03: 'la")
print(" conclusione non si trasferisce fra sleeve').")
print(" ⚠️ Il che NON assolve close-only in generale: sul canale PROP/FUNDED, dove il")
print(" vincolo binding e' esattamente una regola di daily-loss per-conto, la lente")
print(" accoppiata resta obbligatoria e la leva li' e' un'altra decisione.")
# ------------------------------------------------------------------ (4)
sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)")
s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED)
print(f"\n versato in {ANNI} anni: ${s500['versato']:,.0f}. 'anni' = mediana CONDIZIONATA")
print(" all'arrivo (non-arrivo = +infinito, MAI -1), stampata accanto alla sua probabilita'.")
print(f"\n {'k':>6}" + "".join(f"{'anni ' + n:>16}{'P<=20a ' + n[:5]:>15}" for n in LENTI)
+ f"{'maxDD acc.':>12}{'P(DD>30) acc.':>15}")
for j, k in enumerate(KS):
cel = ""
for l in range(2):
a, p = anni_cond(s500["hit"][:, j, l])
cel += f"{fmt(a):>16}{p:>15.0%}"
marca = " <-- LIBRO" if k == 1.00 else ""
print(f" {k:>6.2f}" + cel + f"{np.median(s0['dd'][:, j, 1]):>12.1%}"
f"{(s0['dd'][:, j, 1] > DD_SOGLIA).mean():>15.1%}{marca}")
for l, nome in enumerate(LENTI):
d = s500["hit"][:, KS.index(1.00), l] - s500["hit"][:, KS.index(1.50), l]
fin = np.isfinite(d)
print(f"\n lente {nome:>11}: guadagno APPAIATO 1.00x -> 1.50x = "
f"{float(np.median(d[fin]))/365:.2f} anni (mediana delle differenze, n={fin.sum()})")
print(f"\n ▸ tempo trascorso: {time.time()-t_start:.0f}s")
return dict(s0=s0, s500=s500, dd_close=dd_close, dd_acc=dd_acc, delta_p=delta_p,
D=D, ris_e=ris_e, ris_dd=ris_dd, kk=kk)
# ===========================================================================
# ATTACCO 2 — l'etichetta del rischio
# ===========================================================================
def attacco2() -> None:
from src.data.downloader import load_data
from src.portfolio import sleeves as SL
from src.portfolio.portfolio import weights_tilt_null
from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, realized_vol,
resample_1d, simple_returns, tsmom_blend)
sezione("(5) ATTACCO 2a — LA VOL REALIZZATA DI TP01: verifica INDIPENDENTE")
print("\n Lo scetticato ricostruisce TP01 a mano dentro il proprio script. Qui si legge lo")
print(" sleeve DI PRODUZIONE (`src/portfolio/sleeves._tp01_returns`), che e' la serie con")
print(" cui il libro e' misurato ovunque nel progetto: percorso diverso, stessa domanda.")
tp = SL._tp01_returns().values.astype(float)
att = tp[np.abs(tp) > 1e-12]
print(f"\n vol REALIZZATA di TP01 (sleeve di produzione): {tp.std()*365**.5:.2%}"
f" <- lo scetticato dichiara 12,14%")
print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.")
print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA SPIEGA CIRCA META' DEL DIVARIO, e la meta'")
print(" che manca porta a una conclusione operativa diversa. Lo scetticato scrive:")
print(" «il target vale sulla posizione quando c'e', e TP01 e' long-flat». Misura:")
convs, vols_pred, vols_real, sers, flats = [], [], [], {}, []
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
c = df["close"].values.astype(float)
r = simple_returns(c)
t = TrendPortfolio(**CANONICAL)
bpd = t._bpd(df)
vol = realized_vol(r, t.vol_win_days * bpd, bpd * 365.25)
d = np.clip(tsmom_blend(c, tuple(h * bpd for h in t.horizons_days)), 0, None)
tgt = np.asarray(t.target_series(df), float)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0)
convs.append(float(np.sqrt(np.mean(d[ok] ** 2))))
flats.append(float((d[ok] == 0).mean()))
# errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista
fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25)
okf = ok & np.isfinite(fwd)
vols_pred.append(float(np.mean(vol[okf]))); vols_real.append(float(np.mean(fwd[okf])))
print(f"\n {a}: frazione di barre FLAT (direzione 0) {float((d[ok] == 0).mean()):>6.1%}")
print(f" direzione media QUANDO NON FLAT "
f"{float(d[ok][d[ok] > 0].mean()):>6.2f} (tsmom_blend e' la MEDIA di 3 segni: "
f"vale 1/3, 2/3 o 1)")
print(f" fattore di convinzione sqrt(E[d²]) {convs[-1]:>6.3f}"
f" -> da solo porta 20% a {0.20*convs[-1]:.2%}")
print(f" vol prevista (30g trailing) {vols_pred[-1]:>6.2%} vs vol realizzata nei "
f"30g SEGUENTI {vols_real[-1]:>6.2%} (rapporto {vols_real[-1]/vols_pred[-1]:.3f})")
J = pd.concat(sers, axis=1, join="inner").fillna(0.0)
vb, ve = J["BTC"].std() * 365 ** .5, J["ETH"].std() * 365 ** .5
comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5
div = comb / (0.5 * vb + 0.5 * ve)
conv = float(np.mean(convs))
per_leg = (0.5 * vb + 0.5 * ve) / 0.20
p_flat = float(np.mean(flats))
f_flat = float(np.sqrt(1.0 - p_flat))
f_conv = conv / f_flat if f_flat > 0 else float("nan")
print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}")
print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})")
print(f"\n SCOMPOSIZIONE ESATTA (i due fattori si moltiplicano e chiudono il conto):")
print(f" 20,00% ×{per_leg:.3f} (dentro la GAMBA) ×{div:.3f} (diversificazione BTC/ETH)"
f" = {0.20*per_leg*div:.2%}")
print(f" misurato sullo sleeve di produzione: {tp.std()*365**.5:.2%}")
print(f"\n E il ×{per_leg:.3f} della gamba si spacca a sua volta in DUE meta' quasi uguali:")
print(f" · sta FLAT nel {p_flat:.1%} delle barre (direzione 0) -> ×{f_flat:.3f}")
print(f" · quando NON e' flat sta a convinzione PARZIALE (rms {conv/f_flat:.2f}) -> ×{f_conv:.3f}")
print(f" prodotto sqrt(E[d²]) = ×{conv:.3f}; il residuo fino a ×{per_leg:.3f} e'")
print(f" la correlazione fra convinzione e vol realizzata/prevista"
f" (previsore di vol da solo: ×{np.mean(vols_real)/np.mean(vols_pred):.3f}).")
print(f"\n 📌 QUELLO CHE LO SCETTICATO NON DICE, e sono DUE cose.")
print(f" (i) «TP01 e' long-flat» spiega **circa META'** del divario (×{f_flat:.2f}), non")
print(f" il divario. L'altra meta' e' che `tsmom_blend` e' la MEDIA DEI SEGNI di 3")
print(f" orizzonti: quando TP01 e' a mercato sta quasi sempre a 1/3 o 2/3 di")
print(f" convinzione (×{f_conv:.2f}). E la differenza NON e' accademica: se il divario")
print(f" fosse solo 'flat', alzare target_vol non toccherebbe i giorni senza")
print(f" posizione; essendo CONVINZIONE, alzarlo aumenta la size proprio nei giorni")
print(f" in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3 d'accordo).")
print(f" (ii) un terzo fattore non sta nemmeno nello sleeve: ×{div:.2f} viene dal")
print(f" COMBINARE BTC ed ETH a 50/50 (corr {J['BTC'].corr(J['ETH']):.2f}). Non e' un")
print(f" difetto d'etichetta: e' diversificazione, ed e' esattamente cio' che si")
print(f" vuole. Chiamare 'sovrastima del rischio' anche questo pezzo confonde una")
print(f" proprieta' desiderata con un errore di misura.")
print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:")
print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}")
for nm, fn, dic in (("TP01", SL._tp01_returns, "20%"), ("XS01", SL._xsec_returns, "20%"),
("VRP01", SL._vrp_combo_returns, "—"), ("SKH01", SL._skyhook_returns, "nessuno"),
("GTAA01", SL._gtaa_daily_returns, "—")):
v = fn().values.astype(float)
a = v[np.abs(v) > 1e-12]
print(f" {nm:>9}{dic:>19}{v.std()*365**.5:>16.2%}{a.std()*365**.5:>17.2%}"
f"{len(a)/len(v):>9.1%}")
print("\n XS01 dichiara lo STESSO 20% e ne realizza 20,59%: il divario non e' una")
print(" proprieta' del vol-targeting, e' una proprieta' di TP01 (blend di segni +")
print(" long-only + due gambe correlate 0,8). L'affermazione 'l'etichetta sovrastima il")
print(" rischio' e' vera per TP01 e FALSA per il resto del libro.")
# -------------------------------------------------------------- 2b
sezione("(6) ATTACCO 2b — `target_vol` e `k` sono la STESSA leva? (misurato, non dedotto)")
print("\n Non e' ovvio: `target_vol` agisce DENTRO il vol-targeting di TP01 (e interagisce")
print(" col leverage cap 2x), `k` agisce FUORI su tutto il libro. Due differenze")
print(" strutturali attese, e la seconda e' quella che conta.")
skh = SL._skyhook_returns()
def libro(tv: float, lev: float = 2.0) -> tuple[pd.Series, float, float]:
"""(serie di libro 0.75*TP01(tv)+0.25*SKH01, scala ottenuta su TP01, quota barre al cap)"""
ser, atcap = {}, []
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
t = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev})
tgt = np.nan_to_num(np.asarray(t.target_series(df), float))
r = simple_returns(df["close"].values.astype(float))
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
ser[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean()))
Jt = pd.concat(ser, axis=1, join="inner").fillna(0.0)
tp_s = pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index)
if tp_s.index.tz is None:
tp_s.index = tp_s.index.tz_localize("UTC")
sk = skh.copy()
if sk.index.tz is None:
sk.index = sk.index.tz_localize("UTC")
B = pd.concat({"TP01": tp_s, "SKH01": sk}, axis=1, join="inner").fillna(0.0)
return (W_TP * B["TP01"] + W_SKH * B["SKH01"], float(tp_s.std() / 1.0),
float(np.mean(atcap)))
base_s, tp_vol_base, _ = libro(0.20)
print(f"\n {'via':>28}{'scala TP01':>12}{'vol libro':>11}{'drift':>9}{'Sharpe':>8}"
f"{'maxDD':>8}{'peso EFFETTIVO TP01/SKH':>26}{'barre al cap':>14}")
def riga(nome: str, s: pd.Series, scala_tp: float, atcap: float, w_eff: str) -> None:
v = s.values.astype(float)
eq = np.cumprod(1 + v)
dd = float((1 - eq / np.maximum.accumulate(eq)).max())
print(f" {nome:>28}{scala_tp:>12.2f}{v.std()*365**.5:>11.2%}{v.mean()*365:>9.2%}"
f"{v.mean()/v.std()*365**.5:>8.2f}{dd:>8.1%}{w_eff:>26}{atcap:>14.1%}")
riga("k=1.00, target_vol=20% (LIBRO)", base_s, 1.00, 0.0, "75 / 25")
for tv in (0.25, 0.30, 0.40, 0.60):
s, tpv, ac = libro(tv)
sc = tv / 0.20
w = W_TP * sc / (W_TP * sc + W_SKH)
riga(f"target_vol={tv:.0%} (solo TP01)", s, sc, ac, f"{w*100:.0f} / {(1-w)*100:.0f}")
for k in (1.25, 1.50, 2.00, 3.00):
riga(f"k={k:.2f} (tutto il libro)", base_s * k, k, 0.0, "75 / 25")
print("\n ❌ NON SONO LA STESSA LEVA, e la ragione NON e' il leverage cap (che infatti non")
print(" morde: le barre al cap restano sotto qualche punto percentuale). E':")
print(" **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> e'")
print(" simultaneamente una LEVA e un TILT DI PESI. A target_vol=30% il libro non e'")
print(" piu' 75/25 ma ~82/18, e a 60% e' ~90/10.")
print(" Un tilt di pesi in questo progetto ha un gate suo, e non e' opzionale:")
dcols = {"TP01": SL._tp01_returns(), "SKH01": skh}
for nm, s in dcols.items():
if s.index.tz is None:
dcols[nm] = s.tz_localize("UTC")
for tv in (0.30, 0.60):
sc = tv / 0.20
w = W_TP * sc / (W_TP * sc + W_SKH)
try:
g = weights_tilt_null(dcols, {"TP01": W_TP, "SKH01": W_SKH},
{"TP01": round(w, 4), "SKH01": round(1 - w, 4)}, n=300)
print(f" weights_tilt_null 75/25 -> {w*100:.0f}/{(1-w)*100:.0f} "
f"(target_vol {tv:.0%}): gate_pass={g.get('gate_pass')} "
f"delta_insample={g.get('delta_insample'):+.4f} pctl_hold={g.get('pctl_hold'):.0f}")
except Exception as e: # noqa: BLE001
print(f" weights_tilt_null non girato per tv={tv:.0%}: {type(e).__name__}: {e}")
print("\n 📌 CONSEGUENZA CHE LO SCETTICATO NON POTEVA VEDERE dalla sua sezione (5b), che")
print(" guardava TP01 DA SOLO: la sua raccomandazione implicita («il knob target_vol")
print(" funziona, l'ostacolo e' la config») e' vera sullo sleeve e FALSA sul libro.")
print(" Alzare `target_vol` NON e' la strada meno invasiva per alzare k: e' la strada")
print(" che aggiunge un cambio di pesi mai giudicato al cambio di scala.")
print(" La strada pulita per un puro cambio di SCALA e' il cap di config, che e'")
print(" esattamente quella protetta dal test di guardia.")
def attacco2b() -> None:
"""Quali affermazioni pubblicate cambiano, e quali no — misurato, non argomentato."""
from src.portfolio import sleeves as SL
sezione("(6-bis) ATTACCO 2c — QUALI AFFERMAZIONI CAMBIANO (e la maggior parte NON cambia)")
tp, sk = SL._tp01_returns(), SL._skyhook_returns()
if tp.index.tz is None:
tp = tp.tz_localize("UTC")
if sk.index.tz is None:
sk = sk.tz_localize("UTC")
B = pd.concat({"TP01": tp, "SKH01": sk}, axis=1, join="inner").fillna(0.0)
w = np.array([W_TP, W_SKH])
X = B[["TP01", "SKH01"]].values
bk = X @ w
cov = np.cov(X.T)
var_b = float(bk.var())
rc = w * (cov @ w) / (cov @ w @ w)
print("\n NON cambia NIENTE di misurato. Ogni Sharpe, maxDD, muro, traiettoria e rendita")
print(" pubblicati sono calcolati sulla SERIE DI RITORNI REALIZZATA, mai sull'etichetta:")
print(" `target_vol` entra nel codice come parametro di sizing e non compare in nessuna")
print(" formula di rischio a valle. Un'etichetta che sovrastima non rende ottimista un")
print(" numero che non la usa.")
print("\n CAMBIA una classe di affermazioni sola: quelle che CONFRONTANO un parametro con")
print(" una misura. Nel progetto ce n'e' una, ed e' nel libro LIVE:")
print(f"\n {'sleeve':>8}{'peso NOZIONALE':>16}{'vol realizzata':>16}"
f"{'contributo al RISCHIO':>23}")
for i, nm in enumerate(("TP01", "SKH01")):
print(f" {nm:>8}{w[i]:>16.0%}{X[:, i].std()*365**.5:>16.2%}{rc[i]:>23.1%}")
print(f" {'libro':>8}{1.0:>16.0%}{bk.std()*365**.5:>16.2%}{sum(rc):>23.1%}")
print(f" corr TP01/SKH01 {np.corrcoef(X.T)[0,1]:+.3f} vol libro {bk.std()*365**.5:.2%}")
sig = X.std(axis=0) * 365 ** 0.5
cf = (W_TP * 0.20) ** 2 / ((W_TP * 0.20) ** 2 + (W_SKH * sig[1]) ** 2)
print("\n ❌ IPOTESI MIA, REFUTATA DAL MIO STESSO NUMERO. Mi aspettavo che l'etichetta")
print(" nascondesse una ripartizione del rischio molto diversa dai pesi di nozionale.")
print(f" Non e' cosi': {rc[0]*100:.0f}/{rc[1]*100:.0f} contro {W_TP*100:.0f}/{W_SKH*100:.0f}"
f" — coincidono entro un paio di punti, e per una")
print(" ragione che si legge in una riga: contributi di rischio ~ (w·σ)² con corr")
print(f" {np.corrcoef(X.T)[0,1]:+.2f} ~ 0, e {W_TP:.2f}×{sig[0]:.1%} = {W_TP*sig[0]:.2%} contro"
f" {W_SKH:.2f}×{sig[1]:.1%} = {W_SKH*sig[1]:.2%}")
print(" stanno gia' in rapporto ~75/25. **Coincidenza numerica, non struttura.**")
print("\n 📌 IL FATTO CHE RESTA, ed e' piu' utile dell'ipotesi che ho perso: l'errore non")
print(" lo commette chi legge i PESI, lo commette chi legge l'ETICHETTA. Un lettore che")
print(f" prendesse `target_vol=20%` per buono calcolerebbe TP01 al {cf*100:.0f}% del rischio")
print(f" di libro; il vero e' {rc[0]*100:.0f}%. L'etichetta non rende ottimista nessun numero")
print(" pubblicato (nessuno la usa) — sposta di ~16 punti l'ATTRIBUZIONE del rischio,")
print(" cioe' proprio la domanda 'quale gamba mi fa male' su cui il progetto ha gia'")
print(" tre misure indipendenti che indicano SKH01 come la fragile (LOO d'ancora,")
print(" sensibilita' alla fee 4x, vol realizzata piu' alta).")
print("\n E si precisa una frase: «SKH01 e' l'unica delle 5 a NON essere vol-targeted» e'")
print(" esatta sul CODICE e va letta insieme al fatto che e' anche quella che di vol ne")
print(" realizza di PIU' (21,0% contro 12,1%). Non e' una contraddizione: e' il motivo per")
print(" cui il peso 25% e' stato confermato tre volte all'estremo prudente.")
def guardia() -> None:
sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia")
cfg = json.loads((ROOT / "config" / "live.json").read_text())
frac = cfg["max_notional_per_asset_frac"]
print(f"""
`config/live.json`: max_notional_per_asset_frac = {frac} su 2 asset (BTC, ETH)
-> nozionale LORDO massimo = {frac*2:.2f}x l'equity. k > {frac*2:.2f}x oggi NON e' eseguibile.
`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`
asserisce `frac * len(ASSETS) <= 1.0` E `disaster_sl_pct > 0`.
⚠️ Cio' che protegge NON e' "la leva e' pericolosa": e' la CONCLUSIONE PUBBLICATA del
26/07 secondo cui la **liquidation fee 1%** introdotta da Deribit dal 2026-08-01 e'
irrilevante per questo libro. Quella conclusione poggia interamente sul fatto che a
nozionale lordo <= 1x servirebbe un movimento avverso ~100% per liquidare. Se il cap
sale, la liquidation fee torna nel conto e `r0726_fee_sensitivity` va rifatto.
Il test e' quindi un promemoria di DIPENDENZA, non un divieto: chi alza il cap deve
rifare quell'analisi, non aggirare l'assert.
Nota di taglia, che vale piu' del cap: il conto e' ~$635 e `min_order_usd` = {cfg['min_order_usd']}.
Alzare k moltiplica anche il TURNOVER, e il progetto ha gia' misurato (02/07) che a
questo capitale il min-order $5 e' GIA' la banda di ribilanciamento ottimale.""")
if __name__ == "__main__":
main()
attacco2()
attacco2b()
guardia()
print(f"\n▸ totale: consegna nel messaggio dell'agente.")
+895
View File
@@ -0,0 +1,895 @@
"""MONITOR-AUDIT — quanti dei sei forward-monitor registrano la giornata che credono di registrare?
PERCHE'. Il filone XSR-REPRO (22/08) ha trovato che `paper_xsr.advance()` consuma la barra 1d
del giorno IN CORSO — `fetch_hyperliquid` gira nel cron delle 00:30 con END = oggi e scrive una
barra parziale — e poi porta `last_ts` su quella barra: le 23 ore e mezza restanti di ogni giorno
non entrano in nessun rendimento registrato. Misura: 1/28 barre identiche al replay, corr -0,045,
vol registrata 0,46% contro 2,74% ricalcolata.
Il difetto non e' di XSR01: e' della FORMA `new = [i for i in range(len(ts)) if ts[i] > last_ts]`
applicata a un pannello la cui ultima riga e' provvisoria. Quella forma e' copiata in sei file.
Tre decisioni PRE-REGISTRATE si leggono su queste serie:
STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.5)
XSR01 2026-10-23 (r0725_xsr_deploy_gate: Sharpe >= 1.0 E haircut $5.000 <= 40%)
DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread)
e `monitor_health` dichiara **OK** tutti e sei, perche' misura freschezza e buchi: una serie
fresca, completa e SBAGLIATA passa ogni controllo di freschezza.
QUESTO SCRIPT E' UN AUDIT, NON UNA RIPARAZIONE. E' in SOLA LETTURA su tutto cio' che e'
produzione: importa i moduli di `scripts/live/` senza chiamarne `main()`, e prima di rieseguirne
`advance()` sostituisce `_append` con una funzione che raccoglie in memoria — nessun file di
`data/` viene aperto in scrittura. La riparazione e' una decisione dell'operatore e ha un costo
(azzerare una finestra forward): la sezione E lo quantifica.
METODO — per ogni monitor tre domande, ognuna con una PROVA e non con la lettura di un docstring
(il progetto ha gia' trovato QUATTRO docstring di produzione che dichiaravano una causalita' che
il codice non aveva: `_skyhook_positions`, `resample_5m`, `current_target` di TP01, e
`prevday_breakout.current_target` qui sotto):
A. DA DOVE VIENE L'ULTIMA BARRA. Si misura la COMPLETEZZA dell'ultima riga di ogni sorgente
con un contatore indipendente dal prezzo: quante barre 5m stanno dentro l'ultima barra 1h,
quante barre 1h dentro l'ultimo giorno, quanto volume ha l'ultima barra HL rispetto alla
mediana, quanti giorni di ritardo ha l'ultima barra IB.
B. IL DIFETTO MORDE? Replay della strategia CONGELATA sui dati di oggi dall'inception, contro
cio' che il monitor ha registrato giorno per giorno, appaiato per timestamp. Non "legge una
barra parziale" ma **quanta parte della giornata finisce nella serie registrata**: barre
identiche, correlazione, rapporto di varianza -> minuti al giorno effettivamente misurati.
C. QUALE GATE NE DIPENDE e se il criterio resta leggibile.
Poi:
D. LA GUARDIA CHE MANCA a `monitor_health`, con soglia dichiarata e controllo positivo nei due
versi (un rilevatore tarato per non segnalare e' indistinguibile da uno rotto).
E. IL COSTO DELLA RIPARAZIONE per gate: giorni di finestra persi, data in cui il contatore
nuovo tornerebbe alla stessa numerosita', e se esiste una RICOSTRUZIONE a posteriori.
COSA MI ASPETTAVO PRIMA DI MISURARE, e cosa e' andato diversamente:
* i tre monitor giornalieri su feed crypto (xsr, statarb, dvolspread) con la STESSA patologia,
intorno ai ~35 minuti al giorno di XSR01 -> **meta' giusto**: la patologia e' la stessa ma la
TAGLIA no (2, 4 e 41 min/giorno). Un libro dollar-neutral perde molto piu' di un libro
direzionale a parita' di troncamento, perche' su 35 minuti le due gambe si muovono insieme;
* `paper_prevday` MOLTO piu' mite (1 barra su 24) -> **confermato** (1.438 min/giorno su 1.440),
con un dettaglio non previsto: il difetto si propaga alla barra successiva quando il bersaglio
calcolato sulla barra parziale differisce;
* `paper_combo` rotto o sano per caso -> **sano, e per caso**: aspetta la chiusura di una BORSA;
* NON previsto: le uniche barre corrette di `paper_statarb` esistono perche' il feed si ROMPE
per 7 giorni a luglio e il monitor le recupera CHIUSE. Sono anche la prova che serviva per
dire che la finestra e' ricostruibile.
nice -n 19 timeout 900 uv run python scripts/research/r0822_monitor_audit.py
"""
from __future__ import annotations
import csv
import importlib.util
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(_p))
ANN = np.sqrt(365.0)
NOW = datetime.now(timezone.utc)
# --------------------------------------------------------------------------------------------
# Caricamento SOLA LETTURA di un modulo di produzione: si esegue il modulo (definizioni), mai
# `main()`. `_append` viene sostituito subito dopo, prima di qualunque chiamata ad `advance`.
# --------------------------------------------------------------------------------------------
_MODS: dict[str, object] = {}
def live(name: str):
if name in _MODS:
return _MODS[name]
spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
_MODS[name] = m
return m
class Sink:
"""Sostituisce `_append` dei monitor: raccoglie in memoria invece di scrivere su disco.
E' l'unica ragione per cui questo audit puo' rieseguire il codice di produzione TALE E QUALE
(nessuna reimplementazione -> nessun drift fra audit e monitor) restando in sola lettura."""
def __init__(self):
self.rows: list[tuple[str, dict]] = []
def __call__(self, path, rec):
self.rows.append((Path(path).name, dict(rec)))
def of(self, fname: str) -> list[dict]:
return [r for n, r in self.rows if n == fname]
def read_jsonl(p: Path) -> list[dict]:
if not p.exists():
return []
return [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
def read_equity_csv(p: Path, col: int = 1) -> tuple[list[pd.Timestamp], np.ndarray]:
"""(date, equity) da un equity.csv dei paper_portfolio / paper_combo."""
dts, eq = [], []
with p.open() as fh:
for row in csv.reader(fh):
if not row or row[0] == "date":
continue
try:
v = float(row[col])
except (ValueError, IndexError):
continue
dts.append(pd.Timestamp(row[0]))
eq.append(v)
return dts, np.asarray(eq, float)
# --------------------------------------------------------------------------------------------
# Statistica del confronto registrato-vs-replay. La domanda non e' "coincide?" ma "quanta parte
# della giornata e' finita nella serie": in radice di tempo il rapporto di VARIANZA fra le due
# serie e' la frazione di giornata misurata (ipotesi: random walk sull'intervallo troncato).
# --------------------------------------------------------------------------------------------
def compare(rec: np.ndarray, rep: np.ndarray, mask: np.ndarray | None = None) -> dict:
ok = np.isfinite(rec) & np.isfinite(rep)
if mask is not None:
ok &= mask
a, b = rec[ok], rep[ok]
n = len(a)
if n < 3:
return dict(n=n, ident=0, corr=float("nan"), vr=float("nan"), minuti=float("nan"),
sd_rec=float("nan"), sd_rep=float("nan"), sh_rec=float("nan"), sh_rep=float("nan"))
ident = int((np.abs(a - b) <= 1e-6).sum())
corr = float(np.corrcoef(a, b)[0, 1]) if a.std() > 0 and b.std() > 0 else float("nan")
vr = float((a.std() / b.std()) ** 2) if b.std() > 0 else float("nan")
return dict(n=n, ident=ident, corr=corr, vr=vr, minuti=vr * 1440.0,
sd_rec=float(a.std() * ANN * 100), sd_rep=float(b.std() * ANN * 100),
sh_rec=float(a.mean() / a.std() * ANN) if a.std() > 0 else float("nan"),
sh_rep=float(b.mean() / b.std() * ANN) if b.std() > 0 else float("nan"))
def show(T: pd.DatetimeIndex, rec: np.ndarray, rep: np.ndarray, note: str = "") -> dict:
"""Stampa il confronto e ritorna la statistica sulla finestra PULITA (senza l'incidente)."""
c = compare(rec, rep)
fuori = ~((T >= FREEZE[0]) & (T <= FREEZE[1]))
cf = compare(rec, rep, mask=fuori)
print(f" barre appaiate {c['n']} identiche entro 1e-6: {c['ident']}/{c['n']} "
f"corr {c['corr']:+.4f}")
print(f" vol annualizzata REGISTRATA {c['sd_rec']:6.2f}% RICALCOLATA {c['sd_rep']:6.2f}%"
f" rapporto di varianza {c['vr']:.4f}")
print(f" -> il monitor misura ~{c['minuti']:,.0f} minuti di mercato al giorno (su 1440){note}")
if cf["n"] != c["n"]:
print(f" ESCLUSO l'incidente del feed 09-15/07 ({c['n'] - cf['n']} barre recuperate a "
f"posteriori, quindi CHIUSE):")
print(f" barre {cf['n']} identiche {cf['ident']}/{cf['n']} corr {cf['corr']:+.4f}"
f" vol reg. {cf['sd_rec']:.2f}% vs ric. {cf['sd_rep']:.2f}%"
f" -> ~{cf['minuti']:,.0f} min/giorno")
print(f" Sharpe della finestra: REGISTRATO {c['sh_rec']:+.2f} RICALCOLATO {c['sh_rep']:+.2f}"
f" [SE ~{np.sqrt(365.0 / max(c['n'], 1)):.2f}: NON e' una lettura di gate]")
return dict(all=c, clean=cf)
RES: dict[str, dict] = {} # riepilogo per la tabella finale
# --------------------------------------------------------------------------------------------
# L'INCIDENTE DEL FEED (2026-07-09 -> 2026-07-15). Non e' un'assunzione: e' documentato nel
# sorgente di produzione (`rebuild_history.py`, commento "incidente 2026-07-15: feed fermo 7g" —
# `shutil.copy2` del backup falliva con EPERM e ABORTIVA il rebuild) ed e' verificabile nel log
# del cron. Conta qui per una ragione precisa: durante il congelamento i monitor NON trovavano
# barre nuove, e al ripristino hanno recuperato in un colpo solo barre gia' CHIUSE — cioe' le
# uniche barre corrette delle loro serie. Vanno escluse da ogni stima del difetto, o si finisce
# per attribuire al difetto un'attenuazione che viene da un guasto diverso.
FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC"))
CRONLOG = ROOT / "logs" / "cron_daily.log"
def freeze_evidence() -> dict[str, int]:
"""Giorni in cui `rebuild_history` e' ABORTITO nel cron (letto dal log, non assunto)."""
out: dict[str, int] = {}
day = None
lines = CRONLOG.read_text(errors="replace").splitlines() if CRONLOG.exists() else []
for i, ln in enumerate(lines):
if ln.startswith("===== 2026-") and "cron_daily" in ln:
day = ln.split()[1][:10]
if day and "rebuild_history.py" in ln and "Traceback" in "".join(lines[max(0, i - 3):i]):
out[day] = out.get(day, 0) + 1
return out
# ============================================================================================
# A — PROVENIENZA: quanto e' completa l'ultima riga di ogni sorgente
# ============================================================================================
def sezione_A() -> dict:
print("=" * 102)
print(" A — DA DOVE VIENE L'ULTIMA BARRA (completezza misurata sul disco, non dedotta)")
print("=" * 102)
src = {}
print("\n A1. BTC/ETH — il feed 1h certificato (scritto da rebuild_history nel cron delle 00:30)")
for a in ("btc", "eth"):
d1 = pd.read_parquet(RAW / f"{a}_1h.parquet", columns=["timestamp", "volume"])
t1 = pd.to_datetime(d1["timestamp"], unit="ms", utc=True)
d5 = pd.read_parquet(RAW / f"{a}_5m.parquet", columns=["timestamp"])
t5 = pd.to_datetime(d5["timestamp"], unit="ms", utc=True)
last_h = t1.iloc[-1]
n5 = int(((t5 >= last_h) & (t5 < last_h + pd.Timedelta("1h"))).sum())
n1d = int((t1 >= last_h.normalize()).sum())
src[a] = dict(last=last_h, n5=n5, n1d=n1d, mtime=datetime.fromtimestamp(
(RAW / f"{a}_1h.parquet").stat().st_mtime, tz=timezone.utc))
print(f" {a.upper()} ultima barra 1h {last_h} barre 5m dentro: {n5}/12 "
f"(~{n5 * 5} minuti su 60)")
print(f" barre 1h dentro l'ultimo giorno UTC: {n1d}/24 -> la barra 1d che ne "
f"esce dal resample copre ~{n5 * 5 + (n1d - 1) * 60} minuti su 1440")
print(f" parquet scritto alle {src[a]['mtime']:%H:%M:%S} UTC")
print(" `resample_tf` (src/strategies/trend_portfolio.py:195) NON scarta la barra in corso —")
print(" lo dichiara il suo stesso docstring — e `altlib.get(asset,'1d')` ci passa dentro.")
print("\n A2. Hyperliquid 1d (scritto da fetch_hyperliquid nello stesso cron, END = oggi)")
pq = RAW / "hl_btc_1d.parquet"
d = pd.read_parquet(pq)
v = d["volume"].astype(float).values
tsl = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC")
mt = pd.Timestamp(pq.stat().st_mtime, unit="s", tz="UTC")
q = float(v[-1] / np.median(v[-40:-1]))
orolog = (mt - tsl).total_seconds() / 60.0
src["hl"] = dict(last=tsl, frac=q, min_orologio=orolog)
print(f" hl_btc_1d ultima barra {tsl} parquet scritto alle {mt:%H:%M:%S} UTC")
print(f" misura da OROLOGIO (diretta): la barra era aperta da {orolog:.0f} minuti su 1440 "
f"quando e' stata scritta")
print(f" misura da VOLUME (proxy): {v[-1]:,.0f} = {q * 100:.1f}% del volume mediano di "
f"un giorno pieno -> {q * 1440:,.0f} minuti")
print(" ⚠ i due stimatori NON coincidono (fattore ~2) e la discrepanza va detta invece che")
print(" mediata: il volume dei primi 30 minuti UTC non e' 1/48 di quello giornaliero")
print(" (stagionalita' intragiornaliera). Il numero da usare e' quello da OROLOGIO — e'")
print(" una misura diretta del tempo trascorso; il volume e' solo un controllo di segno.")
print("\n A3. ETF/IB 1d (fetch_ib_equities, stesso cron ~00:35 UTC) e DVOL")
for sym in ("spy", "tlt"):
p = RAW / f"eq_{sym}_1d.parquet"
if not p.exists():
continue
e = pd.read_parquet(p)
last = pd.Timestamp(int(e["timestamp"].iloc[-1]), unit="ms", tz="UTC")
lag = (pd.Timestamp(NOW).normalize() - last.normalize()).days
src[f"eq_{sym}"] = dict(last=last, lag=lag)
print(f" eq_{sym}_1d ultima barra {last.date()} = {lag} giorni fa "
f"(la borsa USA chiude alle 20:00 UTC: alle 00:35 l'ultima barra e' COMPLETA)")
for a in ("btc", "eth"):
p = RAW / f"dvol_{a}.parquet"
if p.exists():
dv = pd.read_parquet(p)
tl = pd.Timestamp(int(dv["timestamp"].iloc[-1]), unit="ms", tz="UTC")
print(f" dvol_{a} ultima barra {tl}")
print("\n LETTURA: tre sorgenti su quattro consegnano un'ultima riga PROVVISORIA (crypto 1h,")
print(" crypto 1d, HL 1d); la quarta (IB) e' l'unica che consegna solo barre chiuse, e non per")
print(" merito del nostro codice ma perche' la borsa e' chiusa quando il cron gira.")
return src
# ============================================================================================
# B — IL DIFETTO MORDE? replay contro registrato, monitor per monitor
# ============================================================================================
def b_xsr():
print("\n" + "=" * 102)
print(" B1 — paper_xsr (CONTROLLO: difetto gia' stabilito da r0822_xsr_repro, qui replicato)")
print("=" * 102)
px = live("paper_xsr")
st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl")
sink = Sink(); px._append = sink
ts, dt, W, R, rb, syms = px.build_panel()
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"],
modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])),
reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS})
px.advance(st0)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
for r in rec if int(r["ts"]) in rep]
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
print("\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)")
print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.")
out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs]))
RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23",
src="hl_*_1d.parquet", cad_h=24.0, ric="SI bit-exact",
rec=rec, rep=sink.of("returns.jsonl"))
def b_statarb():
print("\n" + "=" * 102)
print(" B2 — paper_statarb (gate PRE-REGISTRATO 2026-09-27)")
print("=" * 102)
ps = live("paper_statarb")
st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl")
j = ps.build_joint("1d")
ts, dt, pos, sr = ps._signal(j)
i0 = int(np.where(ts == st["start_ts"])[0][0])
sink = Sink(); ps._append = sink
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0,
pos_modeled=float(pos[i0]), pos_real=float(pos[i0]),
cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL,
peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
ps.advance(st0, j)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
for r in rec if int(r["ts"]) in rep]
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs])
print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` "
"(ultima riga = 1 barra 1h su 24)")
out = show(T, A, B)
ident = np.abs(A - B) <= 1e-6
if ident.any():
gg = sorted({str(x.date()) for x in T[ident]})
print(f"\n 📌 LE UNICHE BARRE CORRETTE DELLA SERIE VENGONO DA UN GUASTO: le {int(ident.sum())} "
f"identiche\n sono CONSECUTIVE ({gg[0]} -> {gg[-1]}) e cadono dentro l'incidente "
"del feed.")
ev = sorted(freeze_evidence())
print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in "
f"{len(ev)} giri consecutivi")
print(f" ({ev[0]} -> {ev[-1]}), EPERM sul backup — difetto gia' riparato nel sorgente.")
print(" Col feed fermo il monitor non trovava barre nuove; al ripristino ne ha")
print(" recuperate 7 in un colpo solo, di cui 6 GIA' CHIUSE -> coincidono al bit.")
print(" Due conseguenze: (a) il 'min/giorno' globale e' GONFIATO da quelle barre — la")
print(" riga 'ESCLUSO l'incidente' e' la stima onesta; (b) quelle 6 barre sono la prova")
print(" che, su barre chiuse, il codice di produzione riproduce il replay ESATTAMENTE.")
RES["paper_statarb"] = dict(parziale="SI", cmp=out["clean"], gate="STATARB 2026-09-27",
src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI bit-exact",
rec=rec, rep=sink.of("returns.jsonl"))
def b_dvolspread():
print("\n" + "=" * 102)
print(" B3 — paper_dvolspread (kill PRE-REGISTRATO 2026-10-24, decisione 2027-01-24)")
print("=" * 102)
pv = live("paper_dvolspread")
st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl")
P = pv._panel()
i0 = int(np.where(P["ts"] == st["start_ts"])[0][0])
sink = Sink(); pv._append = sink
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0,
n_flat_signal=0, n_flat_nodata=0,
wb_modeled=float(P["wb"][i0]), wb_real=float(P["wb"][i0]),
cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL,
peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN)
pv.advance(st0, P)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
for r in rec if int(r["ts"]) in rep]
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`")
print(f" inception {T[0].date()} = dopo l'incidente del feed: finestra gia' pulita, "
"nessuna barra recuperata.")
out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs]))
print("\n ⚠ E' IL PEGGIORE DEI SEI, e non e' un caso: il libro e' uno SPREAD BTC-ETH e su 35")
print(" minuti le due gambe si muovono quasi insieme -> la parte idiosincratica, che e' cio'")
print(" che questo libro tenta di catturare, e' quasi tutta ancora da venire quando la barra")
print(" viene letta. Un libro dollar-neutral perde piu' di un libro direzionale a parita' di")
print(" troncamento — il numero di sopra non e' 35/1440 ma un ordine di grandezza sotto.")
print("\n ⚠ La contabilita' a 3 stati (ATTIVE / flat-da-segnale / flat-SENZA-DATO) e il veto")
print(" d'integrita' all'80% funzionano e restano validi: contano se il DVOL c'era. Non")
print(" possono vedere QUANTO dura la barra su cui il DVOL c'era — sono ortogonali a questo")
print(" difetto, ed e' il motivo per cui l'unico monitor con una guardia dedicata e'")
print(" compromesso quanto quelli senza.")
RES["paper_dvolspread"] = dict(parziale="SI", cmp=out["clean"],
gate="DVOLSPREAD 2026-10-24 / 2027-01-24",
src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0,
ric="SI bit-exact", rec=rec, rep=sink.of("returns.jsonl"))
def b_prevday():
print("\n" + "=" * 102)
print(" B4 — paper_prevday (griglia ORARIA: la patologia puo' essere diversa — si misura)")
print("=" * 102)
pp = live("paper_prevday")
st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl")
dfs = pp.build_bars()
S = st["start_ts"]
pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS}
sink = Sink(); pp._append = sink
st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos),
cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL,
peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
pp.advance(st0, dfs)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
for r in rec if int(r["ts"]) in rep]
A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs])
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, "
"nessun resample")
out = show(T, A, B, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata")
diff = np.abs(A - B) > 1e-6
print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} "
f"({diff.mean() * 100:.1f}%), |scarto| mediano "
f"{np.median(np.abs(A - B)[diff]):.2e} su |ritorno| mediano {np.median(np.abs(B)):.2e}")
vc = pd.Series(T.hour[diff]).value_counts().sort_index()
print(" ora UTC delle barre divergenti: " +
", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in vc.items()))
g = pd.DataFrame(dict(day=T.date, d=diff)).groupby("day")["d"].sum()
z, uno, due = (g == 0), (g == 1), (g == 2)
print(f" per giorno: {int(uno.sum())} giorni con 1 barra divergente, {int(due.sum())} con 2, "
f"{int(z.sum())} con 0 (totale {len(g)} giorni)")
print(f" i {int(due.sum())} giorni con DUE sono sempre 00:00 + 01:00 = il difetto si PROPAGA "
"una barra avanti,")
print(" perche' il bersaglio calcolato sulla barra parziale e' la posizione TENUTA in quella")
print(" dopo. (Il docstring di `prevday_breakout.current_target` dice 'ultima barra chiusa':")
print(" quinto docstring di produzione che dichiara una causalita' che il codice non ha.)")
zg = [str(x) for x in g[z].index]
print(f" i {int(z.sum())} giorni con ZERO divergenze: {', '.join(zg)}")
print(" = inception + oggi + i 7 giorni dell'incidente del feed (barre recuperate CHIUSE).")
print(f"\n ⚠ LA META' CHE CONTA DI PIU': {int((~diff).sum())} barre su {len(A)} coincidono BIT "
"A BIT col replay")
print(" fatto oggi, dopo 62 notti di riscrittura del parquet. E' la PROVA che il feed BTC/ETH")
print(" certificato non tocca le barre gia' chiuse — senza la quale la ricostruzione della")
print(" sezione E sarebbe un'ipotesi invece di un fatto.")
print(" 📌 E il verdetto operativo e' l'opposto degli altri quattro: su griglia oraria il difetto")
print(" c'e' ma vale ~0.1% della varianza. `paper_prevday` NON e' da rigenerare.")
RES["paper_prevday"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (lead in monitor)",
src="btc/eth 1h grezzo", cad_h=1.0, ric="SI bit-exact")
def b_portfolio():
print("\n" + "=" * 102)
print(" B5 — paper_portfolio (book di RICERCA a 5 sleeve: TP01+XS01+VRP01+SKH01+GTAA01)")
print("=" * 102)
ppf = live("paper_portfolio")
dts, eq = read_equity_csv(ROOT / "data" / "paper_portfolio" / "equity.csv")
_, r = ppf.portfolio_daily()
ridx = {pd.Timestamp(k): float(v) for k, v in r.items()}
A, B, D = [], [], []
for i in range(1, len(dts)):
d = dts[i]
if d in ridx:
A.append(eq[i] / eq[i - 1] - 1.0)
B.append(ridx[d])
D.append(d)
T = pd.DatetimeIndex(D)
print("\n serie: `StrategyPortfolio(active_sleeves()).combined_daily()`; i rendimenti "
"registrati si\n ricavano da equity.csv (il monitor salva l'equity, non i ritorni)")
A = np.array(A); B = np.array(B)
out = show(T, A, B)
ident = np.abs(A - B) <= 1e-6
zeri = ident & (np.abs(B) <= 1e-12)
if ident.any():
print(f"\n ⚠ TRAPPOLA DA NON PRENDERE: delle {int(ident.sum())} barre 'identiche', "
f"{int(zeri.sum())} sono ZERO contro ZERO")
print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ")")
print(" = giorni in cui TUTTI e cinque gli sleeve erano flat insieme (TP01 risk-off,")
print(" GTAA a borsa chiusa nel weekend, VRP fuori scadenza).")
print(" Contarle come 'coincidono' sarebbe la trappola gia' codificata dal progetto —")
print(" barre ATTIVE, non barre di calendario (26/07, il gate che segnalo' VRP01 perche'")
print(" contava il 94% di zeri). Su barre attive le coincidenze qui sono "
f"{int((ident & ~zeri).sum())}.")
print("\n ⚠ ULTIMA RIGA ESCLUSA DAL GIUDIZIO? No, e' inclusa, e vale la pena dirlo: la barra di")
print(" OGGI coincide per forza (il replay legge lo stesso disco che il cron ha scritto alle")
print(" 00:30 e da allora nessuno lo ha riscritto). Un audit che girasse SOLO sull'ultima")
print(" barra concluderebbe 'tutto a posto'. La potenza sta nelle barre di IERI e prima.")
print(" ⚠ Ricostruzione NON bit-exact per questo monitor: uno dei cinque sleeve e' GTAA01, che")
print(" legge `eq_*_1d` da IB con `ADJUSTED_LAST` — ri-aggiustato all'indietro a ogni stacco")
print(" di dividendo (difetto gia' incontrato il 07/08 su TLT). La finestra si ricostruisce,")
print(" ma la parte equity non tornera' identica al centesimo, e va detto.")
RES["paper_portfolio"] = dict(parziale="SI", cmp=out["clean"],
gate="nessuno (dashboard/book di ricerca)",
src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0,
ric="SI non al bit")
def b_combo():
print("\n" + "=" * 102)
print(" B6 — paper_combo (calendario di BORSA: TP01 Deribit + GTAA IB)")
print("=" * 102)
pc = live("paper_combo")
dts, eq = read_equity_csv(ROOT / "data" / "paper_combo" / "equity.csv", col=1)
naked, _ = pc.both_daily()
ridx = {pd.Timestamp(k): float(v) for k, v in naked.items()}
A, B, D = [], [], []
for i in range(1, len(dts)):
d = dts[i]
if d in ridx:
A.append(eq[i] / eq[i - 1] - 1.0)
B.append(ridx[d])
D.append(d)
A = np.array(A); B = np.array(B); T = pd.DatetimeIndex(D)
print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA")
out = show(T, A, B)
dif = np.abs(A - B)
print(f"\n Le barre non identiche NON sono il difetto della barra parziale: |scarto| mediano "
f"{np.median(dif):.2e}\n contro |ritorno| mediano {np.median(np.abs(B)):.2e} "
f"(= {np.median(dif) / np.median(np.abs(B)) * 100:.0f}% del ritorno tipico, massimo "
f"{dif.max():.2e}) e si concentrano\n nella finestra dell'incidente del feed + nei giorni "
"in cui IB ha ri-aggiustato `ADJUSTED_LAST`.")
tp_last = pd.Timestamp(list(ridx.keys())[-1])
print(f"\n PERCHE' QUESTO E' DIVERSO. La griglia e' `eq.index` (i giorni di borsa di IB),")
print(f" non il calendario crypto: l'ultima riga della serie e' {tp_last.date()}, mentre il")
print(f" feed crypto su disco arriva a oggi. Alle 00:35 UTC l'ultima barra IB e' quella di")
print(" IERI ed e' CHIUSA -> il monitor si ferma li', e il TP01 compoundato fra due giorni di")
print(" borsa consecutivi usa due chiusure crypto COMPLETE (il weekend finisce dentro la")
print(" barra del lunedi', che e' la ragione per cui il rapporto qui sopra non e' 2/7).")
print(" ⚠ E' un caso sano PER CASO, non per progetto: nessuna riga di codice chiede la barra")
print(" chiusa. Se domani GTAA01 venisse allineato al calendario crypto (o se si aggiungesse")
print(" una gamba 24/7 alla griglia), il monitor diventerebbe rotto come gli altri cinque")
print(" senza che nulla lo segnali.")
RES["paper_combo"] = dict(parziale="NO", cmp=out["clean"], gate="nessuno (paper cross-venue)",
src="TP01 1h->1d su griglia IB", cad_h=24.0,
ric="SI non al bit")
# ============================================================================================
# C — TABELLA DI SINTESI
# ============================================================================================
GATE_INFO = {
"paper_xsr": ("XSR01 2026-10-23", "Sharpe>=1.0 E haircut $5.000<=40%"),
"paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.5"),
"paper_dvolspread": ("DVOLSPREAD 24/10 kill", "Sharpe<-0.50 -> ritiro; 27/01 decisione piena"),
"paper_prevday": ("-", "lead senza data"),
"paper_portfolio": ("-", "dashboard"),
"paper_combo": ("-", "paper cross-venue"),
}
def sezione_C():
print("\n" + "=" * 102)
print(" C — TABELLA: una riga per monitor")
print("=" * 102)
print("\n Le colonne sono calcolate ESCLUDENDO l'incidente del feed 09-15/07 (le barre")
print(" recuperate a posteriori sono chiuse e attenuerebbero il difetto per la ragione")
print(" sbagliata). 'min/g' = minuti di mercato al giorno effettivamente misurati, su 1440.")
print(f"\n {'monitor':<19}{'barra parz.':>13}{'ident./tot':>13}{'corr':>9}{'min/g':>8}"
f"{'ricostruibile':>17} gate dipendente")
for k, v in RES.items():
c = v["cmp"]
ratio = "{}/{}".format(c["ident"], c["n"])
print(f" {k:<19}{v['parziale']:>13}{ratio:>13}{c['corr']:>+9.3f}"
f"{c['minuti']:>8,.0f}{v['ric']:>17} {GATE_INFO[k][0]}")
print("\n 'ricostruibile' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare")
print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script.")
print(" 'non al bit' = ricostruibile, ma una gamba viene dal feed equity IB, che e'")
print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`) -> i centesimi non torneranno uguali.")
print("\n LETTURA IN UNA RIGA: quattro monitor su sei registrano fra 2 e 41 minuti di mercato")
print(" al giorno spacciandoli per 24 ore; i due che non lo fanno sono quello a griglia oraria")
print(" (dove il difetto vale una barra su 24) e quello che aspetta la chiusura di una BORSA.")
def sezione_C2():
"""Il criterio di ciascun gate PRE-REGISTRATO resta leggibile? Le soglie NON sono ridichiarate
qui: si IMPORTANO dagli script di gate e dal monitor (regola del progetto — un sorvegliante
deriva il proprio bersaglio dal codice sorvegliato, mai lo ridichiara: 21/08, `fee_watch`)."""
print("\n" + "=" * 102)
print(" C2 — IL CRITERIO DEI TRE GATE PRE-REGISTRATI E' ANCORA LEGGIBILE?")
print("=" * 102)
import r0724_statarb_deploy_gate as GS # noqa: E402 (solo costanti: ha il main-guard)
import r0725_xsr_deploy_gate as GX # noqa: E402
pv = live("paper_dvolspread")
def metriche(rows: list[dict], key_m="net_modeled", key_r=None):
rm = np.array([float(r[key_m]) for r in rows])
eq = np.cumprod(1 + rm)
pk = np.maximum.accumulate(eq)
out = dict(sh=float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0,
dd=float(np.max((pk - eq) / pk)), tot=float(np.prod(1 + rm) - 1))
if key_r:
rr = np.array([float(r.get(key_r, np.nan)) for r in rows])
rr = rr[np.isfinite(rr)]
out["tot_r"] = float(np.prod(1 + rr) - 1) if len(rr) else float("nan")
return out
def coppia(name):
rec = RES[name]["rec"]
rep = {int(r["ts"]): r for r in RES[name]["rep"]}
keep = [r for r in rec if int(r["ts"]) in rep]
return keep, [rep[int(r["ts"])] for r in keep]
print(f"\n {'gate':<26}{'metrica primaria':<26}{'soglia':>10}{'REGISTR.':>11}"
f"{'RICOSTR.':>11} verdetto")
# --- STATARB -------------------------------------------------------------------------
a, b = coppia("paper_statarb")
ma, mb = metriche(a, key_r="net_real"), metriche(b, key_r="net_real")
flip = (ma["sh"] >= GS.SH_DEPLOY) != (mb["sh"] >= GS.SH_DEPLOY)
print(f" {'STATARB ' + str(GS.DECISION):<26}{'Sharpe net_modeled':<26}"
f"{'>= ' + str(GS.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} "
f"{'*** SI RIBALTA ***' if flip else 'stesso'}")
print(f" {'':<26}{'maxDD':<26}{'< ' + f'{GS.DD_MAX:.0%}':>10}{ma['dd']:>10.1%}"
f"{mb['dd']:>11.1%} "
f"{'*** SI RIBALTA ***' if (ma['dd'] < GS.DD_MAX) != (mb['dd'] < GS.DD_MAX) else 'stesso'}")
hp_a = abs((1 + ma["tot"]) - (1 + ma["tot_r"])) * 100
hp_b = abs((1 + mb["tot"]) - (1 + mb["tot_r"])) * 100
print(f" {'':<26}{'fill-haircut cum (pp)':<26}{'< ' + str(GS.HAIRCUT_MAX_PP):>10}"
f"{hp_a:>11.2f}{hp_b:>11.2f} "
f"{'*** SI RIBALTA ***' if (hp_a < GS.HAIRCUT_MAX_PP) != (hp_b < GS.HAIRCUT_MAX_PP) else 'stesso'}")
# --- XSR01 ---------------------------------------------------------------------------
a, b = coppia("paper_xsr")
K5 = "net_REAL-$5000"
ma, mb = metriche(a, key_r=K5), metriche(b, key_r=K5)
hc_a = (ma["tot"] - ma["tot_r"]) / abs(ma["tot"]) if ma["tot"] else float("nan")
hc_b = (mb["tot"] - mb["tot_r"]) / abs(mb["tot"]) if mb["tot"] else float("nan")
fl1 = (ma["sh"] >= GX.SH_DEPLOY) != (mb["sh"] >= GX.SH_DEPLOY)
fl2 = (hc_a <= GX.HAIRCUT_MAX) != (hc_b <= GX.HAIRCUT_MAX)
print(f" {'XSR01 ' + str(GX.DECISION):<26}{'Sharpe net_modeled':<26}"
f"{'>= ' + str(GX.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} "
f"{'*** SI RIBALTA ***' if fl1 else 'stesso'}")
print(f" {'':<26}{'haircut $5.000':<26}{'<= ' + f'{GX.HAIRCUT_MAX:.0%}':>10}"
f"{hc_a:>10.1%}{hc_b:>11.1%} {'*** SI RIBALTA ***' if fl2 else 'stesso'}")
# --- DVOLSPREAD ----------------------------------------------------------------------
a, b = coppia("paper_dvolspread")
ma, mb = metriche(a), metriche(b)
fl = (ma["sh"] < pv.KILL_SHARPE) != (mb["sh"] < pv.KILL_SHARPE)
print(f" {'DVOLSPREAD ' + pv.KILL_DATE:<26}{'Sharpe fwd (kill se sotto)':<26}"
f"{'> ' + f'{pv.KILL_SHARPE:+.2f}':>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} "
f"{'*** SI RIBALTA ***' if fl else 'stesso (KILL in entrambe)'}")
print("""
⚠ QUESTA NON E' UNA LETTURA DEI GATE, e non deve diventarlo: le tre date sono nel futuro
(anticiparle sarebbe selezione-sul-forward, contro la regola del progetto) e con 28-54 barre
l'errore standard di uno Sharpe annualizzato e' 2,6-3,6. Serve a una cosa sola, e la mostra:
il difetto NON e' rumore centrato — sposta la metrica primaria abbastanza da attraversare la
soglia. Su STATARB la serie registrata dice +1,96 (sopra la soglia di deploy 0,5) e quella
ricostruita dice -2,02: **stesso codice, stessa finestra, verdetto opposto**.
⚠ E c'e' un secondo segnale, gratis: uno Sharpe registrato di -15,8 su 28 barre e' IMPOSSIBILE
(|Sharpe| oltre 4 SE). Il gate `implausible_sharpe` esiste gia' in `altlib` dal 26/07 ma
nessuno lo punta sulle serie FORWARD — solo sui candidati. Un numero fuori scala e' un
rilevatore di guasti a costo zero, e in questo caso era li' da leggere da settimane.""")
# ============================================================================================
# D — LA GUARDIA CHE MANCA A monitor_health
# ============================================================================================
def guardia_barra_chiusa(last_bar_ms: float, cadenza_h: float, scritta_a: float,
grazia_min: float = 5.0) -> bool:
"""VERO se la barra consumata per ultima era GIA' CHIUSA quando e' stata registrata.
`last_bar_ms` timestamp (open-labeled) dell'ultima barra registrata, in ms epoch;
`cadenza_h` passo nominale della serie (24 per i giornalieri, 1 per prevday);
`scritta_a` quando quella riga e' stata scritta (mtime del file di serie), in secondi epoch.
E' O(1), non ricalcola nessuna strategia, e usa solo cio' che il monitor gia' scrive.
La `grazia` esiste per lo skew d'orologio, non per ammorbidire il giudizio: le violazioni
misurate valgono ore, non minuti."""
chiusura = last_bar_ms / 1000.0 + cadenza_h * 3600.0
return chiusura <= scritta_a + grazia_min * 60.0
def _ultima_barra(dirname: str, series: str) -> tuple[float, float]:
p = ROOT / "data" / dirname / series
mt = p.stat().st_mtime
if series.endswith(".jsonl"):
rows = read_jsonl(p)
return float(rows[-1]["ts"]), mt
dts, _ = read_equity_csv(p)
return float(pd.Timestamp(dts[-1]).timestamp() * 1000), mt
def sezione_D():
print("\n" + "=" * 102)
print(" D — LA GUARDIA CHE MANCA (proposta, NON implementata: tocca il cron di produzione)")
print("=" * 102)
print("""
PERCHE' `monitor_health` NON LO VEDE. Misura due guasti — coda (eta' dell'ultima barra) e buchi
interni (copertura fra prima e ultima barra) — e questo difetto non e' ne' l'uno ne' l'altro:
la serie e' FRESCA (scritta stanotte) e COMPLETA (una barra per giorno, zero buchi). Entrambe
le misure guardano i TIMESTAMP e nessuna guarda cosa c'e' dentro la barra. Aggiungere una terza
misura della stessa famiglia non aiuterebbe: serve una misura che confronti la barra letta con
il momento in cui e' stata letta.
DUE CANDIDATI, e non sono equivalenti.""")
# ---- candidato 1: vol registrata vs vol ricalcolata --------------------------------------
print("\n D1. CANDIDATO 'RAPPORTO DI VOLATILITA'' (vol registrata / vol ricalcolata).")
print(f" {'monitor':<19}{'vol reg.':>10}{'vol ric.':>10}{'rapporto':>10} verdetto a "
"soglia [0.70, 1.40]")
lo, hi = 0.70, 1.40
miss = []
for k, v in RES.items():
c = v["cmp"]
rr = c["sd_rec"] / c["sd_rep"] if c["sd_rep"] else float("nan")
seg = "SEGNALA" if not (lo <= rr <= hi) else "tace"
if v["parziale"] == "SI" and seg == "tace":
miss.append(k)
print(f" {k:<19}{c['sd_rec']:>9.2f}%{c['sd_rep']:>9.2f}%{rr:>10.3f} {seg}")
rotti = sum(1 for v in RES.values() if v["parziale"] == "SI")
print(f" -> segnala {rotti - len(miss)} dei {rotti} monitor che leggono una barra parziale,")
print(f" NON segnala il sano (`paper_combo`, 0.987), e MANCA {len(miss)}: "
f"{', '.join(miss) if miss else '-'}")
print(" Motivo strutturale, non taratura: su una griglia oraria il difetto tocca 1 barra")
print(" su 24 e sposta la vol dell'1-2%, cioe' DENTRO il rumore di stima di una vol su")
print(" poche settimane. Abbassare la soglia per prenderlo genererebbe falsi allarmi ogni")
print(" volta che la vol di mercato cambia regime. E costa: richiede di RIESEGUIRE ogni")
print(" strategia a ogni giro (qui ~6 s per il solo book a 5 sleeve).")
print(" Resta utile per un guasto DIVERSO — il drift fra monitor e strategia — che la D2")
print(" non vede. Va aggiunta DOPO, non al posto della D2.")
# ---- candidato 2: barra chiusa ------------------------------------------------------------
print("\n D2. CANDIDATO 'BARRA CHIUSA' — RACCOMANDATO.")
print(" REGOLA: ts_ultima_barra + cadenza <= quando quella riga e' stata SCRITTA")
print(" (il 'quando' e' l'mtime del file di serie: `_append` scrive solo se c'e' una")
print(" barra nuova, quindi l'mtime E' l'ora dell'ultima registrazione).")
print(" Soglia: grazia 5 minuti per lo skew d'orologio. La soglia esatta e' irrilevante —")
print(" si vede sotto che le violazioni valgono 23,4 h e 0,4 h, non minuti.")
specs = [("paper_xsr", "returns.jsonl", 24.0), ("paper_statarb", "returns.jsonl", 24.0),
("paper_dvolspread", "returns.jsonl", 24.0), ("paper_prevday", "returns.jsonl", 1.0),
("paper_portfolio", "equity.csv", 24.0), ("paper_combo", "equity.csv", 24.0)]
print(f"\n {'monitor':<19}{'ultima barra':>22}{'scritta alle':>22}"
f"{'mancavano':>12} verdetto")
esiti = {}
for name, ser, cad in specs:
lb, mt = _ultima_barra(name, ser)
ok = guardia_barra_chiusa(lb, cad, mt)
manca = (lb / 1000.0 + cad * 3600.0 - mt) / 3600.0
esiti[name] = ok
print(f" {name:<19}"
f"{str(pd.Timestamp(lb, unit='ms', tz='UTC'))[:19]:>22}"
f"{str(pd.Timestamp(mt, unit='s', tz='UTC'))[:19]:>22}"
f"{max(manca, 0):>11.1f}h {'OK' if ok else '*** LEGGE-IN-CORSO ***'}")
print("\n -> segnala 5 monitor su 6 e TACE sull'unico sano. Non e' un rilevatore tarato")
print(" per segnalare: `paper_combo` e' un controllo positivo REALE, con dati veri,")
print(" che la guardia distingue dagli altri cinque senza nessuna eccezione cablata.")
# ---- controllo positivo sintetico, nei due versi ------------------------------------------
print("\n D3. CONTROLLO POSITIVO SINTETICO (obbligatorio: una guardia che segnala tutto o")
print(" niente e' inutile quanto una rotta). Casi costruiti, nei DUE versi:")
base = pd.Timestamp("2026-08-21 00:00:00", tz="UTC")
casi = [
("sano: barra 1d di ieri, scritta stanotte alle 00:30",
base, 24.0, pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("ROTTO: barra 1d di OGGI, scritta oggi alle 00:30 (= i cinque monitor)",
pd.Timestamp("2026-08-22 00:00", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), False),
("sano: barra 1h delle 23:00, scritta a mezzanotte e mezza",
pd.Timestamp("2026-08-21 23:00", tz="UTC"), 1.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("ROTTO: barra 1h delle 00:00, scritta alle 00:30 (= paper_prevday)",
pd.Timestamp("2026-08-22 00:00", tz="UTC"), 1.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), False),
("sano: monitor in ritardo di 3 giorni (fermo, ma non legge una barra aperta)",
pd.Timestamp("2026-08-18 00:00", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("limite: barra chiusa 4 minuti DOPO la scrittura -> assorbita dalla grazia",
pd.Timestamp("2026-08-21 00:04", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:00", tz="UTC"), True),
]
tutti = True
for desc, bar, cad, w, atteso in casi:
got = guardia_barra_chiusa(bar.timestamp() * 1000, cad, w.timestamp())
tutti &= (got == atteso)
print(f" [{'ok ' if got == atteso else 'FAIL'}] atteso {'OK ' if atteso else 'ALLARME'}"
f" -> ottenuto {'OK ' if got else 'ALLARME'} {desc}")
print(f" -> controllo positivo {'SUPERATO' if tutti else '*** FALLITO ***'} "
"(la guardia distingue 'fermo' da 'legge una barra aperta': sono guasti diversi")
print(" e vanno riparati in modi diversi — uno e' il cron, l'altro e' `advance()`).")
print("\n D4. COSA NON COPRE, dichiarato. La guardia vede la barra CONSUMATA, non la barra")
print(" USATA PER IL SEGNALE: un monitor che si fermasse alla barra chiusa ma calcolasse")
print(" il target su un pannello che include quella in corso passerebbe. Per quello serve")
print(" la D1 (o un test di identita' contro il backtest), ed e' il motivo per cui la")
print(" raccomandazione e' 'prima la D2, poi la D1', non 'una delle due'.")
# ============================================================================================
# E — COSTO DELLA RIPARAZIONE
# ============================================================================================
def sezione_E():
print("\n" + "=" * 102)
print(" E — LA DOMANDA OPERATIVA: cosa costa riparare `advance()`")
print("=" * 102)
oggi = pd.Timestamp(NOW).normalize().tz_localize(None)
righe = [
("paper_statarb", "STATARB", pd.Timestamp("2026-09-27"), "returns.jsonl"),
("paper_xsr", "XSR01", pd.Timestamp("2026-10-23"), "returns.jsonl"),
("paper_dvolspread", "DVOLSPREAD (kill)", pd.Timestamp("2026-10-24"), "returns.jsonl"),
("paper_dvolspread", "DVOLSPREAD (decisione)", pd.Timestamp("2027-01-24"), "returns.jsonl"),
]
print("\n IPOTESI PESSIMISTA — si ripara `advance()` e si fa `--reset` (la finestra riparte da")
print(" zero oggi). Con una barra al giorno, tornare a N barre richiede N giorni:")
print(f"\n {'monitor / gate':<42}{'barre':>7}{'data gate':>13}{'reset -> N barre':>18}"
f"{'ritardo':>10}")
ritardi = []
for mon, nome, gate, ser in righe:
n = len(read_jsonl(ROOT / "data" / mon / ser))
rip = oggi + pd.Timedelta(days=n)
rit = (rip - gate).days
ritardi.append((nome, rit))
print(f" {f'{mon} / {nome}':<42}{n:>7}{str(gate.date()):>13}"
f"{str(rip.date()):>18}{rit:>+9}g")
peggiore = max(ritardi, key=lambda x: x[1])
print(f"\n Anche nell'ipotesi pessimista il danno NON e' uniforme: l'unico gate che slitterebbe")
print(f" e' {peggiore[0]} ({peggiore[1]:+d} giorni) — gli altri hanno finestre cosi' giovani che")
print(" ricostruirebbero la propria numerosita' PRIMA della data di decisione. Ma la colonna")
print(" giusta e' ancora un'altra, e toglie di mezzo anche quel ritardo:")
print("""
✅ LA FINESTRA FORWARD **NON VA PERSA**: e' RICOSTRUIBILE, per tutti e sei i monitor.
Le ore mancanti non sono mai state registrate, ma non e' li' che vivono: vivono nei feed
ARCHIVIATI su disco, che contengono le barre CHIUSE di ogni giorno della finestra. Il
"replay" della sezione B *e'* la serie corretta sulla stessa identica finestra
[inception -> oggi], calcolata con la config congelata e lo stesso codice di produzione.
Ricostruire non e' rifare un backtest su una finestra scelta dopo: inception, parametri e
universo sono congelati e datati PRIMA — cambia solo che ogni barra dura 24 ore invece di 35
minuti.
LE DUE CONDIZIONI, ed e' giusto chiedere che siano verificate e non assunte:
(1) il feed non riscrive le barre gia' chiuse. **MISURATO qui, due volte e in modo
indipendente**: `paper_prevday` ha 1427 barre su 1488 identiche BIT A BIT al replay
dopo 62 notti di riscrittura del parquet BTC/ETH (B4); e le 6 barre che `paper_statarb`
recupero' durante l'incidente del feed coincidono anch'esse al bit (B2) — due percorsi
diversi, stesso verdetto. Per Hyperliquid la misura e' in `r0822_xsr_repro` T2 (Sharpe
2024 e 2025 riprodotti al centesimo su 731 barre).
(2) la config e' congelata e verificabile: `paper_dvolspread` lo controlla da solo
(`frozen` nello stato, esce 1 se diverge); per gli altri il congelamento e' nel
sorgente ed e' in git.
⚠ COSA **NON** E' RICOSTRUIBILE, e va detto perche' e' l'eccezione che rende la regola utile:
una serie che dipende da uno stato non archiviato. Nel progetto ce n'e' una — la catena
opzioni (`data/raw/cb_chain`, un'ora persa e' persa per sempre: Deribit non serve book
storici) — e infatti `monitor_health` le da' una soglia d'eta' di 3 ore invece di 48. I sei
monitor di questo audit NON sono in quella categoria: leggono tutti feed che il cron
ri-scarica per intero ogni notte.
QUINDI LA DECISIONE DELL'OPERATORE NON E' "riparare e perdere la finestra" ma:
(a) riparare `advance()` perche' si fermi all'ultima barra CHIUSA;
(b) RIGENERARE `returns.jsonl` / `equity.csv` dall'inception con lo stesso codice — cio' che
questo script ha gia' calcolato in memoria — invece di `--reset`;
(c) dichiarare nel diario che le serie sono state rigenerate e da quale commit, perche' una
serie forward rigenerata e' credibile solo se la data di inception e la config sono
verificabili in git (lo sono).
Il costo vero e' (c), non i giorni: nessuna delle tre date di gate si sposta.
⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, ma di poco — e il "di poco" e' MISURATO, non
assunto. I libri REAL saltano i ribilanci sotto il min-order e quella decisione dipende dal
capitale corrente: il percorso REAL rigenerato non e' "lo stesso numero calcolato meglio", e'
un secondo percorso deterministico. Contava sapere quanto, perche' il gate XSR01 del 23/10
legge `net_REAL-$5000` per l'haircut: misurato in C2, **1,7% registrato contro 1,8%
ricostruito**. Trascurabile a $5.000 (i ticket superano il min-order comunque). Il caveat
resta vero in linea di principio e va scritto nel diario; non e' un ostacolo alla decisione.
COSA FARE, MONITOR PER MONITOR (raccomandazione, non azione — questo script non tocca nulla).
⚠ Le due cose sono SEPARATE: la RIPARAZIONE di `advance()` (fermarsi all'ultima barra chiusa)
ha senso su tutti e sei — su un monitor sano non cambia niente e lo protegge da un cambio di
griglia futuro; la RIGENERAZIONE della serie e' un'altra decisione, e va fatta solo dove il
difetto morde abbastanza da spostare cio' che si leggera':
paper_xsr rigenerare — 41 min/g, e il gate del 23/10 legge questa serie
paper_statarb rigenerare — 4 min/g, gate 27/09: e' il piu' vicino nel tempo
paper_dvolspread rigenerare — 2 min/g, il peggiore dei sei; kill-check il 24/10
paper_prevday LASCIARE — 1.438/1.440 min/g: la rigenerazione cambierebbe lo 0,1%
della varianza e azzererebbe 1.488 barre di storia
paper_portfolio rigenerare — 19 min/g; nessun gate, ma e' la serie che il dashboard
mostra e che finisce nei diari come "il book sta facendo X"
paper_combo LASCIARE — sano; semmai va PROTETTO con un test che rompa se la sua
griglia smette di essere il calendario di borsa""")
def main() -> None:
print("\n" + "#" * 102)
print("# r0822_monitor_audit — SOLA LETTURA su scripts/live, src/live, data, config, cron")
print(f"# ora UTC {NOW:%Y-%m-%d %H:%M:%S} (il cron_daily di oggi ha girato alle 00:30)")
print("#" * 102 + "\n")
sezione_A()
b_xsr()
b_statarb()
b_dvolspread()
b_prevday()
b_portfolio()
b_combo()
sezione_C()
sezione_C2()
sezione_D()
sezione_E()
print("\n" + "=" * 102)
print(" FINE — nessun file di produzione e' stato modificato da questo script.")
print("=" * 102 + "\n")
if __name__ == "__main__":
main()
+709
View File
@@ -0,0 +1,709 @@
#!/usr/bin/env python
"""r0822_oi_pin.py — OI-PIN: il prezzo e' attratto verso gli strike a massimo open interest
nelle ore che precedono la scadenza Deribit? (ondata 2026-08-22, filone OI-PIN)
CONTESTO / PERCHE' QUESTO FILONE NON E' "CALENDARIO GIA' MORTO"
--------------------------------------------------------------
Il 02/07 il progetto ha bocciato il **calendario** delle scadenze Deribit (drift post-expiry
weekly/monthly/quarterly): 0/24 celle a Bonferroni, e il pattern si INVERTE proprio sul
quarterly, dove l'open interest massimo dovrebbe amplificarlo. Quel test guardava le DATE.
Qui si guarda l'OPEN INTEREST VERO, per strike, ora per ora (`data/raw/cb_chain/`):
il meccanismo proposto non e' "il venerdi' e' speciale" ma "il market maker corto gamma
ricompra/rivende verso lo strike dove sta la sua esposizione". E' un meccanismo NON di
calendario, quindi ammissibile secondo la sezione 5 del brief.
IPOTESI (dichiarata PRIMA di misurare)
--------------------------------------
H1 Il prezzo deriva VERSO il livello a massima concentrazione di OI / gamma / max-pain nelle
ultime ore prima della scadenza (statistica `toward = sign(K - S_t) * r_{t->T}` > 0).
H2 L'effetto e' piu' forte a poche ore dalla scadenza e sulle scadenze grandi (venerdi').
H0-ATTESA DELL'AUTORE: **negativa**. Motivi dichiarati in anticipo:
(a) il crypto e' 24/7, senza il "chiusura di borsa" che genera il pinning azionario classico;
(b) l'OI Deribit e' grande ma il perp/spot che lo dovrebbe muovere e' molto piu' grande;
(c) il progetto ha gia' misurato che ogni effetto di finestra su BTC/ETH e' rumore;
(d) e soprattutto: `K_oi` sta quasi sempre VICINO allo spot, quindi qualunque statistica
"il prezzo va verso K" e' contaminata dalla POSIZIONE del livello, non dal suo OI.
Per questo il null location-matched (regola codificata il 02/07 sui livelli di Fibonacci)
non e' un contorno: e' IL test.
CONVENZIONE CAUSALE (verificata da un assert nel codice)
--------------------------------------------------------
Barre 1h open-labeled: la barra etichettata `tau` copre [tau, tau+1h) e il suo close e' il
prezzo a `tau+1h`.
* snapshot catena all'ora `hh` -> righe con ts in [hh, hh+1h)
* decisione a fine barra `hh` -> prezzo eseguibile = close(hh) = prezzo a hh+1h
* quindi entrata a `hte` ore dalla scadenza <=> snapshot a dte = hte+1 ore
* uscita al prezzo di regolamento = close della barra (E-1h) = prezzo a E (08:00 UTC)
`A.causality_ok` NON e' applicabile (il segnale non e' funzione del solo feed OHLC): la
causalita' e' garantita per costruzione e verificata da `_assert_causalita()`.
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_oi_pin.py
"""
from __future__ import annotations
import glob
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" / "alt"))
import altlib as A # noqa: E402
STORE = ROOT / "data" / "raw" / "cb_chain"
ASSETS = ("BTC", "ETH")
FEE_SIDE_REAL = 0.00035 # 3,5 bps/lato = fee Deribit REALE del libro (7 bps RT)
FEE_SIDE_CONS = A.FEE_SIDE # 5 bps/lato = convenzione conservativa dei backtest del progetto
MAX_H = 50 # ore prima della scadenza da tenere in memoria
HTE_GRID = (1, 2, 3, 6, 12, 24, 36, 48) # ore-alla-scadenza all'INGRESSO
RULES = ("oi", "gex", "mp") # max OI · max gamma*OI · max pain
PLACEBOS_GLOB = ("rand", "mid", "mr7") # non dipendono dalla regola
PLACEBOS_RULE = ("adj", "jit") # costruiti sulla distanza DELLA regola
SEED = 20260822
RNG = np.random.default_rng(SEED)
# =========================================================================== dati
def _files() -> list[str]:
fs = sorted(glob.glob(str(STORE / "*.parquet")))
if not fs:
raise FileNotFoundError(f"{STORE} vuoto o assente")
return fs
def load_near(max_h: int = MAX_H) -> pd.DataFrame:
"""Catena filtrata alle sole ore vicine a scadenza, letta file per file (RAM: 7 GB in due)."""
cols = ["asset", "strike", "option_type", "ts", "exp",
"open_interest", "gamma", "quote_status"]
out = []
for p in _files():
d = pd.read_parquet(p, columns=cols)
d["ts"] = pd.to_datetime(d["ts"], utc=True)
d["exp"] = pd.to_datetime(d["exp"], utc=True)
d["hh"] = d["ts"].dt.floor("h")
d["dte_h"] = (d["exp"] - d["hh"]).dt.total_seconds() / 3600.0
d = d[(d["dte_h"] > 0) & (d["dte_h"] <= max_h)]
if len(d):
out.append(d)
del d
d = pd.concat(out, ignore_index=True)
# una riga presente non e' un dato presente: OI mancante o quota rotta non conta
d = d[d["open_interest"].notna() & (d["open_interest"] > 0)]
d = (d.sort_values("ts")
.drop_duplicates(subset=["asset", "exp", "hh", "strike", "option_type"], keep="last")
.reset_index(drop=True))
return d
def spot_1h() -> dict[str, pd.Series]:
s = {}
for a in ASSETS:
df = A.get(a, "1h")
s[a] = pd.Series(df["close"].to_numpy(float),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
return s
# =========================================================================== livelli
def _max_pain(K: np.ndarray, oi_c: np.ndarray, oi_p: np.ndarray) -> float:
"""Strike che minimizza il payout complessivo verso i detentori (max pain classico)."""
d = K[None, :] - K[:, None] # d[i,j] = K_j - K_i (K_i = candidato)
pay = (np.maximum(-d, 0.0) * oi_c[None, :]).sum(1) + (np.maximum(d, 0.0) * oi_p[None, :]).sum(1)
return float(K[int(np.argmin(pay))])
def levels_table(chain: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame:
"""Un record per (asset, scadenza, ora): i 3 livelli candidati + i 4 placebo di strike
+ le misure di concentrazione. I placebo sono LOCATION-MATCHED (regola 02/07)."""
rows = []
piv = (chain.groupby(["asset", "exp", "hh", "strike", "option_type"], sort=False)
.agg(oi=("open_interest", "sum"), gm=("gamma", "mean")).reset_index())
for (asset, exp, hh), g in piv.groupby(["asset", "exp", "hh"], sort=False):
S = spot[asset].asof(hh) # prezzo a hh+1h = close(barra hh)
if not np.isfinite(S) or S <= 0:
continue
K = np.sort(g["strike"].unique().astype(float))
if len(K) < 3:
continue
oi_c = np.zeros(len(K)); oi_p = np.zeros(len(K)); gx = np.zeros(len(K))
idx = {k: i for i, k in enumerate(K)}
for k, ot, o, gm in zip(g["strike"].to_numpy(float), g["option_type"].to_numpy(),
g["oi"].to_numpy(float), g["gm"].to_numpy(float)):
i = idx[float(k)]
(oi_c if ot == "C" else oi_p)[i] += o
gx[i] += o * (gm if np.isfinite(gm) else 0.0)
oi = oi_c + oi_p
tot = oi.sum()
if tot <= 0:
continue
i_oi = int(np.argmax(oi))
lv = {
"oi": float(K[i_oi]),
"gex": float(K[int(np.argmax(gx))]) if gx.max() > 0 else np.nan,
"mp": _max_pain(K, oi_c, oi_p),
}
# ---- placebo LOCATION-MATCHED, uno per OGNI regola --------------------------------
# ⚠ difetto trovato e corretto in sessione: la prima stesura costruiva `adj`/`jit`
# attorno al SOLO max-OI e poi li usava come null anche per max-pain e gamma. Ma
# |K_oi - S| mediano e' ~2,4% e |K_mp - S| ~1,1%: confrontare mp con un placebo
# tarato sulla distanza di oi NON e' location-matched, e' proprio l'errore che il
# null doveva prevenire. Ogni regola ha ora i placebo alla PROPRIA distanza.
dist = np.abs(K - S)
for nm, kv in list(lv.items()):
if not np.isfinite(kv):
out_adj = out_jit = np.nan
else:
i_r = int(np.argmin(np.abs(K - kv)))
j = i_r + (1 if (i_r == 0 or (i_r < len(K) - 1 and RNG.random() < 0.5)) else -1)
out_adj = float(K[int(np.clip(j, 0, len(K) - 1))])
d0 = abs(kv - S)
cand = np.where((dist >= 0.5 * d0) & (dist <= 1.5 * d0) & (K != kv))[0]
out_jit = float(K[cand[RNG.integers(len(cand))]]) if len(cand) else np.nan
lv[f"adj_{nm}"] = out_adj
lv[f"jit_{nm}"] = out_jit
# placebo GLOBALI (non dipendono dalla regola)
# rand: permutare l'OI fra gli strike e riprenderne l'argmax equivale ESATTAMENTE a
# estrarre uno strike uniforme dalla griglia (l'argmax di un vettore permutato e'
# uniforme sulle posizioni) -> lo si implementa cosi', ed e' esatto, non simulato.
lv["rand"] = float(K[RNG.integers(len(K))])
# mid: centro della griglia quotata = puro artefatto di copertura, zero informazione OI
lv["mid"] = float(0.5 * (K[0] + K[-1]))
rows.append(dict(
asset=asset, exp=exp, hh=hh, dte_h=float((exp - hh).total_seconds() / 3600.0),
S=float(S), n_k=len(K), tot_oi=float(tot),
top_share=float(oi[i_oi] / tot),
hhi=float(((oi / tot) ** 2).sum()),
k_step=float(np.median(np.diff(K))) if len(K) > 1 else np.nan,
**{f"L_{k}": v for k, v in lv.items()},
))
return pd.DataFrame(rows)
# =========================================================================== il FATTO
def build_events(lev: pd.DataFrame, spot: dict[str, pd.Series]) -> pd.DataFrame:
"""Un evento per (asset, scadenza, hte): livelli, prezzo d'ingresso, prezzo di regolamento."""
ev = []
mr7 = {a: spot[a].rolling(24 * 7, min_periods=24).mean() for a in ASSETS}
for a in ASSETS:
s = spot[a]
sub = lev[lev["asset"] == a]
for exp, g in sub.groupby("exp"):
t_set = pd.Timestamp(exp) - pd.Timedelta(hours=1) # barra il cui close e' a E
if t_set not in s.index:
continue
S_T = float(s.loc[t_set])
gi = g.set_index("hh")
for hte in HTE_GRID:
hh = pd.Timestamp(exp) - pd.Timedelta(hours=hte + 1)
if hh not in gi.index or hh not in s.index:
continue
r = gi.loc[hh]
S = float(s.loc[hh]) # close(hh) = prezzo a E-hte
if not np.isfinite(S) or S <= 0:
continue
rec = dict(asset=a, exp=exp, hte=hte, hh=hh, S=S, S_T=S_T,
ret=S_T / S - 1.0, n_k=int(r["n_k"]), tot_oi=float(r["tot_oi"]),
top_share=float(r["top_share"]), hhi=float(r["hhi"]),
k_step=float(r["k_step"]),
dow=int(pd.Timestamp(exp).dayofweek))
lvls = {c[2:]: r[c] for c in r.index if str(c).startswith("L_")}
lvls["mr7"] = float(mr7[a].asof(hh))
for nm, K in lvls.items():
K = float(K) if K is not None else np.nan
if not np.isfinite(K) or K <= 0:
rec[f"d_{nm}"] = np.nan; rec[f"tw_{nm}"] = np.nan
rec[f"sh_{nm}"] = np.nan
continue
d = (K - S) / S
rec[f"K_{nm}"] = K
rec[f"d_{nm}"] = d
rec[f"tw_{nm}"] = np.sign(d) * rec["ret"] # verso il livello
rec[f"sh_{nm}"] = (abs(S - K) - abs(S_T - K)) / S # avvicinamento
ev.append(rec)
E = pd.DataFrame(ev)
# NULL STATICO CAUSALE (lezione 25/07: il primo null statico di STATARB usava
# sign(mean(segnale)) su tutto il campione = look-ahead; qui la maggioranza e' ESPANDENTE
# e shiftata). Risponde a: "il candidato e' solo 'sempre nello stesso verso' prima della
# scadenza?" — che su 3,7 mesi di mercato direzionale e' l'ipotesi piu' probabile.
E = E.sort_values(["asset", "hte", "hh"]).reset_index(drop=True)
sgn = np.sign(E["d_mp"].to_numpy(float))
maj = (pd.Series(sgn).groupby([E["asset"], E["hte"]])
.transform(lambda x: x.expanding().mean().shift(1)))
E["d_maj"] = np.where(np.isfinite(maj), np.sign(maj) * 0.01, np.nan)
E["tw_maj"] = np.sign(E["d_maj"]) * E["ret"]
E["sh_maj"] = np.nan
E["d_long"] = 0.01; E["tw_long"] = E["ret"]; E["sh_long"] = np.nan
E["d_short"] = -0.01; E["tw_short"] = -E["ret"]; E["sh_short"] = np.nan
return E
def _assert_causalita(chain: pd.DataFrame, ev: pd.DataFrame) -> str:
"""Il livello usato a `hh` deve venire da quote osservate PRIMA del prezzo d'ingresso."""
bad = int((chain["ts"] < chain["hh"]).sum()
+ (chain["ts"] >= chain["hh"] + pd.Timedelta("1h")).sum())
assert bad == 0, "snapshot fuori dalla propria ora"
dte = (pd.to_datetime(ev["exp"]) - pd.to_datetime(ev["hh"])).dt.total_seconds() / 3600.0
assert bool((dte == ev["hte"] + 1).all()), "hte non coerente con lo snapshot"
assert bool(((pd.to_datetime(ev["hh"]) + pd.Timedelta(hours=1))
<= pd.to_datetime(ev["exp"])).all()), "ingresso oltre la scadenza"
return (f"OK — 0/{len(chain)} righe di catena fuori dalla propria ora; "
f"{len(ev)} eventi con dte == hte+1 (ingresso = close(hh), quota vista in [hh,hh+1h))")
# =========================================================================== inferenza
def cluster_boot(x: np.ndarray, clusters: np.ndarray, n: int = 2000, seed: int = SEED):
"""IC95 della media con ricampionamento dei CLUSTER (una scadenza = 1 cluster: BTC ed ETH
della stessa scadenza non sono osservazioni indipendenti)."""
x = np.asarray(x, float)
m = np.isfinite(x)
x, clusters = x[m], np.asarray(clusters)[m]
if len(x) < 5:
return np.nan, np.nan, np.nan, 0
uc = np.unique(clusters)
groups = [x[clusters == c] for c in uc]
rng = np.random.default_rng(seed)
draws = np.empty(n)
for i in range(n):
pick = rng.integers(0, len(groups), len(groups))
draws[i] = np.mean(np.concatenate([groups[j] for j in pick]))
return float(np.mean(x)), float(np.percentile(draws, 2.5)), float(np.percentile(draws, 97.5)), len(uc)
def mde(x: np.ndarray, clusters: np.ndarray) -> float:
"""Effetto minimo rilevabile (alpha .05 bilaterale, potenza 80%) con questo campione."""
x = np.asarray(x, float)
m = np.isfinite(x)
x, cl = x[m], np.asarray(clusters)[m]
if len(x) < 3:
return np.nan
nc = max(len(np.unique(cl)), 1)
return float(2.802 * np.std(x) / np.sqrt(nc))
# =========================================================================== strategia
_W: dict = {}
def win_1h(asset: str, lo: pd.Timestamp, hi: pd.Timestamp) -> pd.DataFrame:
"""Feed 1h tagliato alla finestra della catena: fuori da li' la posizione e' zero per
costruzione, e valutare 70k barre 288 volte costerebbe minuti per nulla."""
key = (asset, lo, hi)
if key not in _W:
df = A.get(asset, "1h")
dt = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
m = (dt >= lo - pd.Timedelta("2D")) & (dt <= hi + pd.Timedelta("2D"))
_W[key] = df.loc[m].reset_index(drop=True)
return _W[key]
def hourly_target(ev: pd.DataFrame, asset: str, rule: str, hte: int,
band: tuple[float, float] | None, sizing: str,
subsample: str, df1h: pd.DataFrame) -> np.ndarray:
"""Posizione oraria: entra `hte` ore prima della scadenza nel verso del livello, esce a
scadenza. Se piu' finestre si sovrappongono vince la scadenza PIU' VICINA (una posizione
alla volta = cio' che il conto Deribit puo' davvero fare: strumento unico, netting)."""
idx = pd.DatetimeIndex(pd.to_datetime(df1h["datetime"], utc=True))
pos = np.zeros(len(idx))
loc = pd.Series(np.arange(len(idx)), index=idx)
e = ev[(ev["asset"] == asset) & (ev["hte"] == hte)].copy()
if subsample == "fri":
e = e[e["dow"] == 4]
elif subsample == "big":
e = e[e["tot_oi"] >= e["tot_oi"].median()]
d = e[f"d_{rule}"].to_numpy(float)
keep = np.isfinite(d)
if band is not None:
keep &= (np.abs(d) >= band[0]) & (np.abs(d) <= band[1])
e = e[keep]
d = d[keep]
if len(e) == 0:
return pos
w = np.sign(d) if sizing == "sign" else np.clip(d / 0.01, -1.0, 1.0)
order = np.argsort(e["exp"].to_numpy())[::-1] # lontane prima: la scadenza VICINA sovrascrive
for j in order:
hh = pd.Timestamp(e["hh"].iloc[j]); exp = pd.Timestamp(e["exp"].iloc[j])
last = exp - pd.Timedelta(hours=2)
if hh not in loc.index or last not in loc.index:
continue
i0, i1 = int(loc[hh]), int(loc[last])
if i1 >= i0:
pos[i0:i1 + 1] = w[j]
return pos
def strat_daily(ev: pd.DataFrame, rule: str, hte: int, band, sizing: str, subsample: str,
fee_side: float) -> tuple[pd.Series, dict]:
"""Serie giornaliera netta 50/50 BTC+ETH + metriche, sulla sola finestra della catena."""
nets, info = {}, {}
lo_w = pd.Timestamp(ev["hh"].min()); hi_w = pd.Timestamp(ev["exp"].max())
for a in ASSETS:
df = win_1h(a, lo_w, hi_w)
tgt = hourly_target(ev, a, rule, hte, band, sizing, subsample, df)
r = A.eval_weights(df, tgt, fee_side=fee_side)
nets[a] = pd.Series(r["net"], index=r["idx"])
info[a] = int(np.sum(np.abs(np.diff(np.r_[0.0, tgt])) > 1e-12))
J = pd.concat(nets, axis=1, join="inner").fillna(0.0)
daily = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
lo = pd.Timestamp(ev["hh"].min()).normalize()
daily = daily[daily.index >= lo]
return daily, info
def metr(s: pd.Series) -> dict:
r = s.dropna()
if len(r) < 5:
return dict(sharpe=np.nan, dd=np.nan, cagr=np.nan, n=len(r))
eq = np.cumprod(1 + r.to_numpy(float)); pk = np.maximum.accumulate(eq)
yrs = max((r.index[-1] - r.index[0]).total_seconds() / 86400 / 365.25, 1e-6)
return dict(sharpe=A._sh(r), dd=float(np.max((pk - eq) / pk)),
cagr=float(eq[-1] ** (1 / yrs) - 1), n=int(len(r)))
# =========================================================================== main
def main() -> None:
P = print
P("=" * 100)
P("OI-PIN — pinning verso gli strike a massimo open interest (catena Deribit oraria)")
P("=" * 100)
# ---------------------------------------------------------------- 1. DATO
chain = load_near()
spot = spot_1h()
lev = levels_table(chain, spot)
ev = build_events(lev, spot)
P("\n[1] DATO")
P(f" righe catena a <= {MAX_H}h dalla scadenza : {len(chain):,}")
P(f" scadenze distinte : {ev.groupby('asset').exp.nunique().to_dict()}")
P(f" finestra : {ev.hh.min()} -> {ev.exp.max()}")
P(f" eventi (asset x scadenza x hte) : {len(ev)}")
P(f" strike per snapshot mediana/min : {lev.n_k.median():.0f} / {lev.n_k.min():.0f}")
P(f" causalita' : {_assert_causalita(chain, ev)}")
P(" ⚠ il collettore scarta gli strumenti con OI<100 -> la griglia e' TRONCATA sulle code:")
P(" il max-OI ne soffre poco (e' un argmax), il MAX PAIN si', perche' e' una somma su tutta")
P(" la catena. Il verdetto su 'mp' va letto come 'max pain sulla catena OI>=100'.")
# ---------------------------------------------------------------- 2. POTENZA (PRIMA del risultato)
P("\n[2] POTENZA DICHIARATA — calcolata PRIMA di leggere qualunque effetto")
P(" Unita' statistica = una SCADENZA (BTC ed ETH della stessa data non sono indipendenti).")
P(f" {'hte':>4} {'n oss':>6} {'n clus':>7} {'sd(ret)':>9} {'MDE medio/trade':>16} {'MDE ann.':>10}")
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
m = mde(e["ret"].to_numpy(float), e["exp"].to_numpy())
ann = m * (365.25 * 24 / hte) if np.isfinite(m) else np.nan
P(f" {hte:>4} {len(e):>6} {e.exp.nunique():>7} {np.nanstd(e['ret']):>9.4f}"
f" {m:>16.4f} {ann:>9.1%}")
P(" LETTURA OBBLIGATORIA: il campione rileva solo effetti ENORMI. A 24h l'MDE e' ~0,5-0,7%")
P(" per trade contro 0,07% di fee: un pinning realistico (pochi bps) e' INVISIBILE qui.")
P(" Quindi un 'non significativo' NON prova assenza; un 'significativo' sarebbe cosi' grande")
P(" da essere sospetto. Il verdetto massimo raggiungibile e' LEAD.")
# ---------------------------------------------------------------- 3. IL FATTO
P("\n[3] IL FATTO — dove sta il livello e dove va il prezzo")
P(" |ret| verso la scadenza e distanza |d| del livello (mediane):")
P(f" {'hte':>4} {'|ret| med':>10} {'|d_oi| med':>11} {'|d_gex|':>9} {'|d_mp|':>9}"
f" {'d_oi<0':>8} {'top_share':>10}")
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
P(f" {hte:>4} {e['ret'].abs().median():>10.4f} {e['d_oi'].abs().median():>11.4f}"
f" {e['d_gex'].abs().median():>9.4f} {e['d_mp'].abs().median():>9.4f}"
f" {(e['d_oi'] < 0).mean():>8.2f} {e['top_share'].median():>10.3f}")
P(" ⚠ se 'd_oi<0' e' lontano da 0.50 il livello ha un BIAS DI SEGNO (griglia troncata o")
P(" mercato in trend): una statistica 'il prezzo va verso K' lo erediterebbe.")
P("\n Statistica di pinning tw = sign(K-S)*ret (media, IC95 cluster-bootstrap su scadenze):")
P(f" {'hte':>4} {'rule':>5} {'media tw':>10} {'IC95 lo':>9} {'IC95 hi':>9} {'sig':>4}"
f" {'shrink med':>11} {'n':>5}")
fact = []
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
for rl in RULES:
m, lo, hi, nc = cluster_boot(e[f"tw_{rl}"].to_numpy(float), e["exp"].to_numpy())
sg = "***" if (np.isfinite(lo) and lo > 0) else ("neg" if (np.isfinite(hi) and hi < 0) else "-")
P(f" {hte:>4} {rl:>5} {m:>10.5f} {lo:>9.5f} {hi:>9.5f} {sg:>4}"
f" {np.nanmedian(e[f'sh_{rl}']):>11.5f} {int(np.isfinite(e[f'tw_{rl}']).sum()):>5}")
fact.append(dict(hte=hte, rule=rl, mean=m, lo=lo, hi=hi))
F = pd.DataFrame(fact)
P(f" celle con IC95 tutto sopra zero: {int((F.lo > 0).sum())}/{len(F)}"
f" | tutto sotto zero: {int((F.hi < 0).sum())}/{len(F)}")
P("\n LA STESSA STATISTICA SUI LIVELLI PLACEBO — se sale anche li', NON e' pinning:")
P(f" {'hte':>4} " + " ".join(f"{('tw_' + n):>10}" for n in
("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short")))
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
P(f" {hte:>4} " + " ".join(
f"{np.nanmean(e[f'tw_{n}']):>10.5f}" for n in
("oi", "mp", "rand", "jit_mp", "mid", "mr7", "maj", "short")))
P("\n QUANTO SONO LO STESSO SEGNALE? accordo del SEGNO di (K-S) fra max-pain e i placebo:")
for n in ("mid", "mr7", "oi", "maj"):
agr = [float(np.nanmean(np.sign(ev.loc[ev.hte == h, "d_mp"])
== np.sign(ev.loc[ev.hte == h, f"d_{n}"]))) for h in HTE_GRID]
P(f" mp vs {n:<5}: " + " ".join(f"h{h}={a:.2f}" for h, a in zip(HTE_GRID, agr)))
P(f" base rate sign<0 : " + " ".join(
f"{n}={np.nanmean(np.sign(ev[f'd_{n}']) < 0):.2f}" for n in ("oi", "gex", "mp", "mid", "mr7")))
for a in ASSETS:
sp_ = spot[a]
w = sp_[(sp_.index >= ev.hh.min()) & (sp_.index <= ev.exp.max())]
P(f" deriva {a} nella finestra: {w.iloc[-1] / w.iloc[0] - 1:+.1%}"
f" (se il livello sta quasi sempre da un lato, il candidato e' una scommessa direzionale)")
# ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED
P("\n[4] NULL LOCATION-MATCHED (regola 02/07: e' la POSIZIONE o e' l'OI?)")
P(" Confronto APPAIATO per evento: tw(livello speciale) - tw(placebo alla stessa distanza).")
P(" placebo PER REGOLA: adj_r = strike adiacente a K_r · jit_r = strike arbitrario a")
P(" distanza 0.5-1.5x |K_r - S| (analogo del Fib±jitter del 02/07).")
P(" placebo GLOBALI: rand = strike uniforme (== permutazione dell'OI, equivalenza esatta) ·")
P(" mid = centro della griglia quotata (zero info OI) · mr7 = media 7g dello spot.")
P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}"
f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}")
npos = ntot = 0
npos_c = ntot_c = 0 # solo i null PULITI (bilanciati nel segno): rand/mid/mr7
for hte in HTE_GRID:
e = ev[ev["hte"] == hte]
for rl in RULES:
cells = []
for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB:
dd = e[f"tw_{rl}"].to_numpy(float) - e[f"tw_{p}"].to_numpy(float)
m, lo, hi, _ = cluster_boot(dd, e["exp"].to_numpy())
sig = bool(np.isfinite(lo) and lo > 0)
cells.append(f"{m:>11.5f}{'*' if sig else ' '}")
ntot += 1; npos += int(sig)
if p in PLACEBOS_GLOB:
ntot_c += 1; npos_c += int(sig)
P(f" {hte:>4} {rl:>5} " + " ".join(cells))
P(f" celle (rule x hte x placebo) in cui lo SPECIALE batte il placebo con IC95>0:"
f" {npos}/{ntot} (atteso per caso ~{0.025 * ntot:.0f})")
P(f" ... contando SOLO i null bilanciati nel segno (rand/mid/mr7): {npos_c}/{ntot_c}"
f" (atteso ~{0.025 * ntot_c:.0f}) <-- e' QUESTO il numero da citare")
P("\n ⚠ UN PLACEBO SI CONTROLLA PRIMA DI USARLO: se il suo segno non e' bilanciato rispetto")
P(" alla regola, il Δ non misura l'informazione, misura il ROVESCIAMENTO del segnale.")
P(f" quota di eventi con sign(K_placebo-S) == sign(K_regola-S), su tutti gli hte:")
for rl in RULES:
agr = {p: float(np.nanmean(np.sign(ev[f"d_{rl}"]) == np.sign(ev[f"d_{p}"])))
for p in (f"adj_{rl}", f"jit_{rl}") + PLACEBOS_GLOB}
P(" " + rl + ": " + " ".join(f"{k}={v:.2f}" for k, v in agr.items()))
P(" un null PULITO sta vicino a 0.50 (rand/mid/mr7). Sotto ~0.35 il placebo e' la")
P(" strategia INVERTITA e il suo Δ va letto come ~2x il segnale, non come un test.")
P(" LETTURA: `adj_r`/`jit_r` chiedono 'e' proprio QUELLO strike?'; `mid`/`mr7` chiedono")
P(" 'serve l'open interest, o basta il centro recente del prezzo?'. La seconda e' la")
P(" domanda che uccide o salva il filone.")
# ---------------------------------------------------------------- 5. CONDIZIONAMENTO
P("\n[5] CONDIZIONAMENTO — la concentrazione di OI e le scadenze grandi cambiano qualcosa?")
P(f" {'hte':>4} {'gruppo':>16} {'media tw_oi':>12} {'IC95 lo':>9} {'IC95 hi':>9} {'n':>4}")
for hte in (3, 12, 24):
e = ev[ev["hte"] == hte]
hi_c = e["top_share"] >= e["top_share"].median()
big = e["tot_oi"] >= e["tot_oi"].quantile(0.75)
for nm, sel in (("conc ALTA", hi_c), ("conc BASSA", ~hi_c),
("OI top-25%", big), ("venerdi'", e["dow"] == 4)):
x = e.loc[sel, "tw_oi"].to_numpy(float)
m, lo, hh_, _ = cluster_boot(x, e.loc[sel, "exp"].to_numpy())
P(f" {hte:>4} {nm:>16} {m:>12.5f} {lo:>9.5f} {hh_:>9.5f} {int(np.isfinite(x).sum()):>4}")
# ---------------------------------------------------------------- 6. STRATEGIA
P("\n[6] STRATEGIA — expectancy netta fee (3,5 bps/lato reali) e serie giornaliera")
bands = {"nessuna": None, "0.2-3%": (0.002, 0.03)}
subs = ("all", "big", "fri")
sizings = ("sign", "prop")
cells, n_trials = [], 0
for rl in RULES:
for hte in HTE_GRID:
for bn, bd in bands.items():
for sz in sizings:
for sb in subs:
n_trials += 1
d, info = strat_daily(ev, rl, hte, bd, sz, sb, FEE_SIDE_REAL)
mm = metr(d)
cells.append(dict(rule=rl, hte=hte, band=bn, sizing=sz, sub=sb,
**mm, ntrade=sum(info.values())))
C = pd.DataFrame(cells)
P(f" griglia strategia valutata: {n_trials} celle "
f"({len(RULES)} rule x {len(HTE_GRID)} hte x {len(bands)} bande x {len(sizings)} sizing x {len(subs)} sottocampioni)")
P(f" celle con Sharpe > 0: {(C.sharpe > 0).sum()}/{len(C)} (una famiglia di RUMORE sta a ~50%)")
P(" migliori 8 celle per Sharpe (NON e' una selezione: e' il MASSIMO della griglia, che serve al DSR):")
P(C.sort_values("sharpe", ascending=False).head(8).to_string(index=False,
float_format=lambda v: f"{v:.3f}"))
P(" peggiori 3:")
P(C.sort_values("sharpe").head(3).to_string(index=False, float_format=lambda v: f"{v:.3f}"))
P("\n [6-bis] LA STESSA MACCHINA SUI LIVELLI PLACEBO (stesse finestre, stessa fee, stesso")
P(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.")
nulls = []
for rl in ("mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"):
for hte in (3, 6, 12, 24):
for bn, bd in bands.items():
d, _i = strat_daily(ev, rl, hte, bd, "sign", "all", FEE_SIDE_REAL)
mm = metr(d)
nulls.append(dict(rule=rl, hte=hte, band=bn, sharpe=mm["sharpe"],
cagr=mm["cagr"], dd=mm["dd"]))
N = pd.DataFrame(nulls)
piv = N.pivot_table(index=["hte", "band"], columns="rule", values="sharpe")
P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"])
.to_string(float_format=lambda v: f"{v:6.2f}"))
P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)")
best = C.sort_values("sharpe", ascending=False).iloc[0]
P(f"\n CELLA MIGLIORE = {best['rule']}/hte{int(best['hte'])}/{best['band']}/{best['sizing']}/{best['sub']}")
# expectancy per trade, in % e in R, sulla cella migliore
e = ev[ev["hte"] == int(best["hte"])].copy()
if best["sub"] == "big":
e = e[e["tot_oi"] >= e["tot_oi"].median()]
elif best["sub"] == "fri":
e = e[e["dow"] == 4]
dd = e[f"d_{best['rule']}"].to_numpy(float)
keep = np.isfinite(dd)
if best["band"] != "nessuna":
keep &= (np.abs(dd) >= 0.002) & (np.abs(dd) <= 0.03)
tw = e.loc[keep, f"tw_{best['rule']}"].to_numpy(float)
dk = np.abs(dd[keep])
net = tw - 2 * FEE_SIDE_REAL
wins, losses = net[net > 0], net[net < 0]
rr = (wins.mean() / abs(losses.mean())) if len(wins) and len(losses) else np.nan
P(f" trade: {len(net)} · WR {np.mean(net > 0):.1%} · RR medio {rr:.2f}"
f" · WR-knob atteso 1/(1+RR) = {1 / (1 + rr):.1%}" if np.isfinite(rr) else " RR n.d.")
P(f" expectancy NETTA per trade: {np.mean(net):+.5f} ({np.mean(net) * 100:+.3f}%)"
f" | in R (R = distanza dal livello): {np.mean(net / np.maximum(dk, 1e-6)):+.3f} R")
P(f" a fee ZERO: {np.mean(tw):+.5f} · la fee costa {2 * FEE_SIDE_REAL:.5f}/trade ="
f" {200 * FEE_SIDE_REAL / max(np.mean(tw), 1e-9):.0f}% del lordo"
f" -> {'MORTE PER FEE' if np.mean(tw) <= 2 * FEE_SIDE_REAL else 'la fee NON e il vincolo'}")
dbest, info = strat_daily(ev, best["rule"], int(best["hte"]),
None if best["band"] == "nessuna" else (0.002, 0.03),
best["sizing"], best["sub"], FEE_SIDE_REAL)
dcons, _ = strat_daily(ev, best["rule"], int(best["hte"]),
None if best["band"] == "nessuna" else (0.002, 0.03),
best["sizing"], best["sub"], FEE_SIDE_CONS)
mb, mc = metr(dbest), metr(dcons)
P(f" serie giornaliera cella migliore fee 3,5bps: Sharpe {mb['sharpe']:+.2f}"
f" · maxDD {mb['dd']:.2%} · CAGR {mb['cagr']:+.2%} · n={mb['n']}g")
P(f" fee 5,0bps: Sharpe {mc['sharpe']:+.2f}"
f" · maxDD {mc['dd']:.2%} · CAGR {mc['cagr']:+.2%}")
# scomposizione: per mese e per ERA DI COLLETTORE (bite:research fino al 30/07, poi pyg)
P("\n SCOMPOSIZIONE (regola 22/08: un contributo positivo si scompone prima di crederci)")
mo = dbest.groupby([dbest.index.year, dbest.index.month])
P(" " + " · ".join(f"{y}-{m:02d}: Sh {A._sh(g):+.2f} ret {float(np.prod(1 + g) - 1):+.2%} ({len(g)}g)"
for (y, m), g in mo))
cut = pd.Timestamp("2026-07-30", tz="UTC")
e1, e2 = dbest[dbest.index < cut], dbest[dbest.index >= cut]
P(f" era collettore bite:research (<30/07): Sh {A._sh(e1):+.2f} ret {float(np.prod(1 + e1) - 1):+.2%} ({len(e1)}g)"
f" | pyg (>=30/07): Sh {A._sh(e2):+.2f} ret {float(np.prod(1 + e2) - 1):+.2%} ({len(e2)}g)")
top5 = dbest.sort_values(ascending=False).head(5)
P(f" concentrazione: i 5 giorni migliori valgono {float(np.prod(1 + top5) - 1):+.2%}"
f" su {float(np.prod(1 + dbest) - 1):+.2%} totali"
f" = {100 * np.log1p(top5).sum() / max(np.log1p(dbest).sum(), 1e-9):.0f}% del log-equity")
P(" ⚠ HOLD-OUT: NON ESISTE. La catena parte dal 2026-05-01 e l'hold-out del progetto e'")
P(" 2025-01-01: TUTTA la serie sta dentro l'hold-out, quindi non c'e' nessuna finestra")
P(" fuori campione da mostrare. Questo NON e' un dettaglio: e' il motivo per cui")
P(" 3,7 mesi di catena non possono produrre uno sleeve.")
# ---------------------------------------------------------------- 7. GATE
P("\n[7] GATE")
P(f" causalita' : {_assert_causalita(chain, ev)}")
P(" A.causality_ok : NON GIRATO — non applicabile (il segnale non e' funzione del solo")
P(" feed OHLC; la causalita' e' garantita dalla convenzione hte=dte-1")
P(" e verificata dall'assert qui sopra).")
# DSR: conta TUTTI i trial al rialzo
all_sr = [float(v) for v in C["sharpe"].to_numpy(float) if np.isfinite(v)]
all_sr_wide = all_sr + [float(v) for v in N["sharpe"].to_numpy(float) if np.isfinite(v)]
n_fact = len(HTE_GRID) * len(RULES) * (1 + len(PLACEBOS_GLOB) + len(PLACEBOS_RULE)) + 3 * 4 # celle del fatto + condiz.
trials_tot = len(all_sr_wide) + n_fact
dsr, sr0 = A.deflated_sharpe(mb["sharpe"], all_sr, dbest, dpy=365.25)
dsr_w, _ = A.deflated_sharpe(mb["sharpe"], all_sr_wide, dbest, dpy=365.25)
P(f" deflated Sharpe : DSR {dsr:.3f} (soglia 0.95) · Sharpe-null atteso {sr0:.2f}"
f" · trial contati {len(all_sr)} (griglia strategia)")
P(f" DSR contando ANCHE le celle placebo ({len(all_sr_wide)} trial): {dsr_w:.3f}")
P(f" trial TOTALI dichiarati al rialzo (fatto+null+condiz.+strategia): {trials_tot}")
# marginal vs TP01
mv = A.marginal_vs_tp01(dbest)
P(f" marginal vs TP01 : {mv.get('marginal_verdict')} · corr {mv.get('corr_full')}"
f" · uplift w25 full {mv.get('blends', {}).get('w25', {}).get('uplift_full')}"
f" · giorni in comune {mv.get('n_days')}")
P(" ⚠ il gate NON PUO' dare ADDS con questo campione: `multicut_persistent`")
P(" richiede >=2 tagli annuali da >=120 giorni e la serie ne ha ~113 in UN")
P(" anno solo; e `has_insample_edge` e' True per DEFAULT perche' non c'e'")
P(" alcun pre-2025. Il verdetto marginale qui e' strutturale, non informativo.")
# null del de-levering (obbligatorio su ogni claim di DD)
B = A.tp01_baseline_daily()
J = pd.concat({"B": B, "C": dbest}, axis=1, join="inner").dropna()
if len(J) > 20:
bl = 0.75 * J["B"] + 0.25 * J["C"]
dd_bl = A._dd_ret(bl)
ks = np.linspace(0.05, 1.0, 96)
ok = [(k, A._sh(k * J["B"]), A._dd_ret(k * J["B"])) for k in ks]
cand = [(k, s) for k, s, d_ in ok if d_ <= dd_bl]
kbest = max(cand, key=lambda t: t[1]) if cand else None
P(f" null de-levering : blend 0.75TP01+0.25cand -> Sharpe {A._sh(bl):+.2f} / DD {dd_bl:.2%}"
f" | k*TP01 a pari DD -> k={kbest[0]:.2f} Sharpe {kbest[1]:+.2f}"
if kbest else " null de-levering : non calcolabile")
if kbest:
P(f" esito: {'REFUTED (il de-levering fa meglio)' if kbest[1] >= A._sh(bl) else 'superato'}")
else:
P(" null de-levering : NON GIRATO (sovrapposizione con TP01 troppo corta)")
# implausible
imp = A.implausible_sharpe(dbest, n_trades=int(best["ntrade"]))
P(f" implausible_sharpe : implausible={imp['implausible']} · {imp.get('reasons')}")
# anchor: l'ANCORA di questa strategia e' hte (quale ora prima della scadenza si entra)
ab = A.anchor_luck_band(
lambda h: strat_daily(ev, best["rule"], int(h),
None if best["band"] == "nessuna" else (0.002, 0.03),
best["sizing"], best["sub"], FEE_SIDE_REAL)[0],
offsets=list(HTE_GRID), canonical=int(best["hte"]))
P(f" anchor (hte) : canonico {ab.get('canonical'):.2f} al {100 * ab.get('canonical_pctl', np.nan):.0f}° pctl"
f" · MEDIANA ONESTA {ab.get('median'):.2f} · banda [{ab.get('lo'):.2f},{ab.get('hi'):.2f}]"
f" · positive {ab.get('frac_positive'):.0%} · gate_pass={ab.get('gate_pass')}")
# LA DOMANDA DECISIVA, sulla banda d'ancora: max-pain aggiunge qualcosa a un livello
# che NON usa opzioni? Mediana delle DIFFERENZE APPAIATE sui 8 hte (mai differenza
# delle mediane: lezione 26/07).
def _fn(rule):
return lambda h: strat_daily(ev, rule, int(h), (0.002, 0.03), "sign", "all",
FEE_SIDE_REAL)[0]
P(" ⚠ le 8 ancore hte NON sono 8 osservazioni indipendenti: guardano le STESSE 73 scadenze")
P(" e lo STESSO percorso di prezzo. '8/8 positive' dice che il segno non dipende")
P(" dall'ancora, NON che l'effetto sia significativo — quello lo dice il cluster-bootstrap")
P(" della sezione [4], che sui placebo senza opzioni (mid/mr7) NON e' significativo.")
for other in ("mr7", "mid", "oi", "rand"):
dl = A.anchor_luck_delta(_fn("mp"), _fn(other), offsets=list(HTE_GRID))
P(f" Δ appaiato mp-{other:<4}: mediana {dl.get('median_paired'):+.2f}"
f" · positiva in {dl.get('n_positive')}/{dl.get('n_anchors')} hte"
f" · banda [{dl.get('lo'):+.2f},{dl.get('hi'):+.2f}] · gate_pass={dl.get('gate_pass')}")
# eseguibilita' a $600
P(" eseguibilita' $600 :", end=" ")
hc = []
for a in ASSETS:
df = win_1h(a, pd.Timestamp(ev["hh"].min()), pd.Timestamp(ev["exp"].max()))
tgt = hourly_target(ev, a, best["rule"], int(best["hte"]),
None if best["band"] == "nessuna" else (0.002, 0.03),
best["sizing"], best["sub"], df)
sc = A.eval_weights_smallcap(df, tgt, capital=300.0, fee_side=FEE_SIDE_REAL)
hc.append(f"{a} haircut {sc['sharpe_haircut']:+.3f} ({sc['n_executed_trades']} trade eseguiti)")
P(" · ".join(hc))
P(" (300$/asset = il cap per-asset del libro live a questo capitale;")
P(" una gamba ±1 muove 300$ >> min-order 5$ -> nessun ordine saltato)")
# ---------------------------------------------------------------- 8. SINTESI
P("\n[8] SINTESI")
n_fact_pos = int((F.lo > 0).sum())
P(f" fatto : {n_fact_pos}/{len(F)} celle (rule x hte) con pinning significativo a IC95")
P(f" null loc. : {npos}/{ntot} confronti appaiati vinti dal livello 'speciale';"
f" {npos_c}/{ntot_c} contando solo i null PULITI")
P(f" di questi, contro il placebo SENZA OPZIONI (media 7g dello spot): "
f"{sum(1 for h in HTE_GRID for rl in RULES if (lambda t: np.isfinite(t[1]) and t[1] > 0)(cluster_boot((ev[ev.hte == h][f'tw_{rl}'] - ev[ev.hte == h]['tw_mr7']).to_numpy(float), ev[ev.hte == h]['exp'].to_numpy())))}/24")
P(f" strategia : miglior Sharpe della griglia {mb['sharpe']:+.2f} su {mb['n']} giorni, DSR {dsr:.3f}")
P(f" marginale : {mv.get('marginal_verdict')} (strutturalmente non ADDS-abile a 3,7 mesi)")
P("=" * 100)
if __name__ == "__main__":
main()
+684
View File
@@ -0,0 +1,684 @@
#!/usr/bin/env python3
"""r0822_ortho_screen.py — ONDATA 2026-08-22, filone ORTHO-SCREEN (la rete larga, onesta).
NON e' un'ipotesi: e' un SETACCIO. Sette famiglie di segnale mai (o solo parzialmente)
coperte dalle ondate precedenti, passate TUTTE dallo stesso protocollo, senza eccezioni:
1. famiglia DICHIARATA prima (parametri + celle), trial contati AL RIALZO;
2. A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe di famiglia;
3. A.marginal_vs_tp01 -> ADDS / NEUTRAL / DILUTES / HEDGE / NOISE / REDUNDANT;
4. A.causality_ok (+ A.day_boundary_robust dove ha senso, vedi NOTA CONFINE);
5. banda d'ancora su 8 offset orari (bar 1d ricostruite da 1h) -> la stima onesta e' la MEDIANA;
6. null del DE-LEVERING su ogni claim di drawdown;
7. A.eval_weights_smallcap($600) + A.implausible_sharpe sulla cella scelta;
8. **DSR di SCREEN**: oltre al deflated-Sharpe di famiglia, il deflated-Sharpe calcolato sul
POOL di TUTTE le celle di TUTTE le famiglie. Se scelgo il migliore fra 7 famiglie, i trial
sono 168, non 24: e' l'unico conto onesto per un setaccio.
PRIOR DICHIARATO PRIMA DI MISURARE: il soffitto direzionale BTC/ETH misurato e' Sharpe ~1.3 e
in 104 ipotesi + 153 agenti (giugno 2026) NESSUNA e' sopravvissuta alla verifica avversariale.
Mi aspetto 0 CANDIDATI. Il valore consegnato e' **quale gate uccide cosa**, perche' questo
restringe lo spazio della prossima ondata.
LE 7 FAMIGLIE (24 celle ciascuna = 168 trial dichiarati)
RSKEW skew realizzata (da 1h) come PREDITTORE direzionale, non come gate
[nuovo vs blind/agent_40_skewgate, che la usava come FILTRO su un trend]
TACC accelerazione del trend (derivata seconda del log-prezzo) come direzione standalone
VPX relazione volume-prezzo su barre 1d (conferma / divergenza a volume alto e basso)
XDISP dispersione cross-sezionale dei 51 alt HL usata come segnale TIME-SERIES sul mercato
[nuovo vs XS01, dove la dispersione e' un gate sul paniere cross-sectional]
XCORR correlazione realizzata BTC-ETH come STATO di mercato direzionale
[nuovo vs ortho/agent_09 e XAS08, dove la corr gata un book relative-value]
XTAIL struttura dei ritorni dopo uno shock a k-sigma (continuazione vs inversione)
VRAT variance-ratio (persistenza) come CONVINZIONE continua sul TSMOM
[nuovo vs blind/agent_37_hurst, che faceva uno switch BINARIO di modo]
NOTA CONFINE (perche' day_boundary_robust non gira su tutte): il test sposta le ETICHETTE del
calendario lasciando i prezzi fermi. Per un segnale di solo prezzo (TACC/VPX/XTAIL/VRAT) e'
esattamente il controllo giusto e deve dare INVARIANT. Per un segnale che fa merge_asof su una
serie AUSILIARIA (RSKEW da 1h, XDISP da HL, XCORR da 1h BTC/ETH) spostare l'etichetta in AVANTI
sposta il punto di merge nel FUTURO: sarebbe un LEAK indotto dallo strumento, non un controllo.
Per quelle famiglie il test d'ancora corretto e' la RICOSTRUZIONE delle barre a offset orario
(banda d'ancora, punto 5), che gira su tutte e sette.
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_ortho_screen.py
"""
from __future__ import annotations
import os
import sys
import time
from functools import lru_cache
import numpy as np
import pandas as pd
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "alt"))
import altlib as A # noqa: E402
HL_DIR = A.DATA_DIR
ANCHOR_OFFSETS = (0, 3, 6, 9, 12, 15, 18, 21)
W_BLEND = 0.25 # peso del candidato nel blend con TP01 (convenzione altlib)
# ===========================================================================
# INFRASTRUTTURA — barre a offset, merge causale di serie ausiliarie
# ===========================================================================
@lru_cache(maxsize=64)
def bars_at(asset: str, off: int) -> pd.DataFrame:
"""Barre 1d ancorate all'ora `off`, ricostruite dal feed 1h certificato.
off=0 deve riprodurre esattamente A.get(asset,'1d') (controllo in _sanity)."""
h = A.get(asset, "1h").copy()
idx = pd.to_datetime(h["timestamp"], unit="ms", utc=True) - pd.Timedelta(hours=off)
h.index = idx
o = (h.resample("1D", label="left", closed="left")
.agg({"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
.dropna(subset=["open"]))
o["datetime"] = o.index + pd.Timedelta(hours=off)
epoch = pd.Timestamp("1970-01-01", tz="UTC")
o["timestamp"] = ((o["datetime"] - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return o.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
def _bar_ms(df: pd.DataFrame) -> int:
d = pd.Series(df["timestamp"].astype("int64")).diff().median()
return int(d) if np.isfinite(d) and d > 0 else 86_400_000
def merge_aux(df: pd.DataFrame, aux_ts: np.ndarray, aux_val: np.ndarray) -> np.ndarray:
"""Valore di una serie ausiliaria NOTO alla CHIUSURA di ogni barra di df.
`aux_ts` e' gia' il timestamp in cui il valore diventa noto (fine della sua barra).
merge_asof backward sul close della barra -> mai un dato del futuro: eval_weights
tiene target[i] durante la barra i+1, quindi close[i] e' l'ultimo istante lecito."""
close_ts = df["timestamp"].astype("int64").values + _bar_ms(df)
left = pd.DataFrame({"timestamp": close_ts})
right = (pd.DataFrame({"timestamp": np.asarray(aux_ts, dtype="int64"),
"v": np.asarray(aux_val, dtype=float)})
.dropna().sort_values("timestamp").reset_index(drop=True))
if right.empty:
return np.full(len(df), np.nan)
return pd.merge_asof(left, right, on="timestamp", direction="backward")["v"].values
def roll_z(x: np.ndarray, win: int) -> np.ndarray:
s = pd.Series(np.asarray(x, float))
mp = max(20, win // 4)
m = s.rolling(win, min_periods=mp).mean()
sd = s.rolling(win, min_periods=mp).std()
return ((s - m) / sd.where(sd > 0)).values
def exp_pct(x: np.ndarray, min_obs: int = 180) -> np.ndarray:
"""Percentile ESPANDENTE causale (rank fra tutti i valori visti finora, incluso l'attuale)."""
return pd.Series(np.asarray(x, float)).expanding(min_periods=min_obs).rank(pct=True).values
def _shape(u: np.ndarray, shape: str) -> np.ndarray:
u = np.nan_to_num(np.asarray(u, float), nan=0.0)
return np.sign(u) if shape == "sign" else np.clip(u, -1.0, 1.0)
def _sized(direction: np.ndarray, df: pd.DataFrame) -> np.ndarray:
return A.vol_target(np.nan_to_num(direction, nan=0.0), df,
target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
# ===========================================================================
# SERIE AUSILIARIE (calcolate una volta, riusate da tutte le celle)
# ===========================================================================
@lru_cache(maxsize=16)
def skew_aux(asset: str, w_days: int):
"""Skew realizzata dei ritorni 1h su finestra trascinata di w_days giorni.
Valore al bar 1h i -> noto alla sua chiusura (ts_i + 1h)."""
h = A.get(asset, "1h")
r = A.log_returns(h["close"].values.astype(float))
win = w_days * 24
sk = pd.Series(r).rolling(win, min_periods=win // 2).skew().values
return h["timestamp"].astype("int64").values + 3_600_000, sk
@lru_cache(maxsize=4)
def disp_aux():
"""Dispersione cross-sezionale dei 51 alt Hyperliquid (1d, 2024+): deviazione standard
cross-section dei log-ritorni giornalieri. Valore del giorno d noto alla chiusura di d."""
import glob
files = sorted(glob.glob(str(HL_DIR / "hl_*_1d.parquet")))
cols = {}
for f in files:
x = pd.read_parquet(f, columns=["timestamp", "close"])
s = pd.Series(x["close"].values.astype(float),
index=x["timestamp"].astype("int64").values)
cols[os.path.basename(f)] = np.log(s).diff()
M = pd.DataFrame(cols).sort_index()
n_ok = M.notna().sum(axis=1)
disp = M.std(axis=1, ddof=1).where(n_ok >= 15)
ts = M.index.values.astype("int64") + 86_400_000 # noto a fine giornata
return ts, disp.values, int(len(files))
@lru_cache(maxsize=16)
def xcorr_aux(w_days: int):
"""Correlazione realizzata BTC-ETH sui ritorni 1h, finestra trascinata w_days giorni."""
b = A.get("BTC", "1h")[["timestamp", "close"]].rename(columns={"close": "b"})
e = A.get("ETH", "1h")[["timestamp", "close"]].rename(columns={"close": "e"})
J = pd.merge(b, e, on="timestamp", how="inner").sort_values("timestamp")
rb = pd.Series(A.log_returns(J["b"].values.astype(float)))
re = pd.Series(A.log_returns(J["e"].values.astype(float)))
win = w_days * 24
c = rb.rolling(win, min_periods=win // 2).corr(re).values
return J["timestamp"].astype("int64").values + 3_600_000, c
# ===========================================================================
# LE 7 FAMIGLIE — factory(tf=..., **params) -> target(df, asset)
# ===========================================================================
def mk_rskew(tf="1d", w_days=30, z_days=365, shape="lin", sgn=-1):
"""RSKEW: la skew realizzata recente come PREDITTORE del ritorno successivo.
A-priori (Amaya et al., azioni): skew realizzata alta -> ritorno futuro BASSO (sgn=-1)."""
def target(df, asset):
ts, sk = skew_aux(asset, w_days)
v = merge_aux(df, ts, sk)
u = np.clip(roll_z(v, z_days) / 2.0, -1.0, 1.0)
return _sized(sgn * _shape(u, shape), df)
return target
def mk_tacc(tf="1d", H=60, L=20, shape="lin", sgn=1):
"""TACC: accelerazione = variazione della PENDENZA log-prezzo su H barre, misurata a L barre
di distanza. Direzione standalone (non e' il LIVELLO del trend). A-priori sgn=+1."""
def target(df, asset):
c = np.log(df["close"].values.astype(float))
n = len(c)
slope = np.full(n, np.nan)
slope[H:] = (c[H:] - c[:-H]) / H
acc = np.full(n, np.nan)
acc[L:] = slope[L:] - slope[:-L]
u = np.clip(roll_z(acc, 365) / 2.0, -1.0, 1.0)
return _sized(sgn * _shape(u, shape), df)
return target
def mk_vpx(tf="1d", W=30, k=5, mode="confirm", thr=0.5):
"""VPX: volume-prezzo su barre 1d (a 1d il volume Deribit e' informativo, a 5m no).
confirm -> segui il movimento SOLO se il volume conferma (z >= thr)
fade_low -> fai il contrario del movimento avvenuto a volume BASSO (z <= -thr)
fade_high -> fai il contrario del movimento avvenuto a volume ALTO (climax, z >= thr)"""
def target(df, asset):
c = df["close"].values.astype(float)
vol = df["volume"].values.astype(float)
n = len(c)
vz = roll_z(np.log(np.where(vol > 0, vol, np.nan)), W)
rk = np.full(n, np.nan)
rk[k:] = c[k:] / c[:-k] - 1.0
s = np.sign(np.nan_to_num(rk, nan=0.0))
vzf = np.nan_to_num(vz, nan=0.0)
if mode == "confirm":
d = np.where(vzf >= thr, s, 0.0)
elif mode == "fade_low":
d = np.where(vzf <= -thr, -s, 0.0)
else: # fade_high
d = np.where(vzf >= thr, -s, 0.0)
return _sized(d, df)
return target
def mk_xdisp(tf="1d", S=5, trans="z252", shape="lin", sgn=1):
"""XDISP: dispersione cross-sezionale dei 51 alt HL come STATO del mercato (time-series),
non come gate di un paniere cross-sectional. Nessun prior forte sul segno -> entrambi in griglia."""
def target(df, asset):
ts, dsp, _ = disp_aux()
sm = pd.Series(dsp).rolling(S, min_periods=1).mean().values
u = (np.clip(roll_z(sm, 252) / 2.0, -1, 1) if trans == "z252"
else 2.0 * exp_pct(sm, 180) - 1.0)
v = merge_aux(df, ts, u)
return _sized(sgn * _shape(v, shape), df)
return target
def mk_xcorr(tf="1d", W=60, trans="z252", shape="lin", sgn=-1):
"""XCORR: co-movimento BTC-ETH (corr realizzata) come stato direzionale del mercato.
A-priori debole: corr alta = 'tutto il crypto e' un trade solo' = regime tardo -> sgn=-1."""
def target(df, asset):
ts, cr = xcorr_aux(W)
u_aux = (np.clip(roll_z(cr, 252 * 24) / 2.0, -1, 1) if trans == "z252"
else 2.0 * exp_pct(cr, 180 * 24) - 1.0)
v = merge_aux(df, ts, u_aux)
return _sized(sgn * _shape(v, shape), df)
return target
def mk_xtail(tf="1d", ksig=3.0, N=3, sgn=1, sigwin=30):
"""XTAIL: struttura dei ritorni ESTREMI. Dopo |r| > ksig*sigma trascurata, tieni per N barre
la direzione (sgn=+1 continuazione) o il suo opposto (sgn=-1 inversione).
ATTENZIONE dichiarata: sgn=-1 e' mean-reversion, famiglia gia' MORTA -> se vince li', si chiude."""
def target(df, asset):
c = df["close"].values.astype(float)
r = A.simple_returns(c)
sd = pd.Series(r).rolling(sigwin, min_periods=sigwin // 2).std().shift(1).values
n = len(c)
d = np.zeros(n)
hit = np.where(np.isfinite(sd) & (sd > 0) & (np.abs(r) > ksig * sd))[0]
for i in hit:
d[i:min(n, i + N)] = sgn * np.sign(r[i])
return _sized(d, df)
return target
def mk_vrat(tf="1d", q=10, LB=365, conv="gate", lf=True):
"""VRAT: variance-ratio VR(q)=Var(r_q)/(q*Var(r_1)) su finestra LB come CONVINZIONE CONTINUA
sul TSMOM (non uno switch binario di modo come blind/agent_37_hurst).
gate -> pesa il trend solo dove VR>1 (persistente), altrimenti flat
flip -> inverte il trend dove VR<1 (anti-persistente)"""
HZ = (30, 90, 180)
def target(df, asset):
c = df["close"].values.astype(float)
lc = np.log(c)
n = len(c)
r1 = pd.Series(A.log_returns(c))
rq = pd.Series(np.concatenate([np.full(q, np.nan), lc[q:] - lc[:-q]]))
v1 = r1.rolling(LB, min_periods=LB // 2).var()
vq = rq.rolling(LB, min_periods=LB // 2).var()
vr = (vq / (q * v1.where(v1 > 0))).values
dirs = np.zeros(n)
for H in HZ:
m = np.zeros(n)
m[H:] = np.sign(c[H:] / c[:-H] - 1.0)
dirs += m / len(HZ)
if lf:
dirs = np.maximum(dirs, 0.0)
k = np.nan_to_num(vr, nan=1.0) - 1.0
w = np.clip(k * 5.0, 0.0, 1.0) if conv == "gate" else np.clip(k * 5.0, -1.0, 1.0)
return _sized(dirs * w, df)
return target
# ---- griglie DICHIARATE (24 celle ciascuna, 7 famiglie = 168 trial) ----------
def _grid(**axes):
keys = list(axes)
out = [{}]
for k in keys:
out = [dict(o, **{k: v}) for o in out for v in axes[k]]
return out
FAMILIES = [
dict(code="RSKEW", factory=mk_rskew, price_only=False,
grid=_grid(w_days=[14, 30, 60], z_days=[180, 365], shape=["lin", "sign"], sgn=[1, -1])),
dict(code="TACC", factory=mk_tacc, price_only=True,
grid=_grid(H=[20, 60, 120], L=[5, 20], shape=["lin", "sign"], sgn=[1, -1])),
dict(code="VPX", factory=mk_vpx, price_only=True,
grid=_grid(W=[30, 90], k=[5, 20], mode=["confirm", "fade_low", "fade_high"], thr=[0.5, 1.0])),
dict(code="XDISP", factory=mk_xdisp, price_only=False,
grid=_grid(S=[1, 5, 20], trans=["z252", "pct"], shape=["lin", "sign"], sgn=[1, -1])),
dict(code="XCORR", factory=mk_xcorr, price_only=False,
grid=_grid(W=[20, 60, 120], trans=["z252", "pct"], shape=["lin", "sign"], sgn=[1, -1])),
dict(code="XTAIL", factory=mk_xtail, price_only=True,
grid=_grid(ksig=[2.5, 3.0], N=[1, 3, 10], sgn=[1, -1], sigwin=[30, 90])),
dict(code="VRAT", factory=mk_vrat, price_only=True,
grid=_grid(q=[5, 10, 20], LB=[180, 365], conv=["gate", "flip"], lf=[True, False])),
]
# ===========================================================================
# GATE AGGIUNTIVI
# ===========================================================================
def cand_daily_at(fn, off: int) -> pd.Series:
"""Serie giornaliera 50/50 BTC+ETH del candidato con barre ancorate all'ora `off`."""
ser = {}
for a in A.CERTIFIED:
df = bars_at(a, off)
ev = A.eval_weights(df, A._call_target(fn, df, a))
ser[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
return A._to_daily(0.5 * J[A.CERTIFIED[0]] + 0.5 * J[A.CERTIFIED[1]])
def anchor_report(fn) -> dict:
"""Banda d'ancora su 8 offset orari: Sharpe standalone E uplift del blend.
La stima ONESTA e' la MEDIANA della banda, non l'ancora 00:00 (lezione 02/07)."""
band = A.anchor_luck_band(lambda o: cand_daily_at(fn, o), ANCHOR_OFFSETS)
B = A.tp01_baseline_daily()
ups = {}
for o in ANCHOR_OFFSETS:
C = cand_daily_at(fn, o)
J = pd.concat({"B": B, "C": C}, axis=1, join="inner").dropna()
if len(J) > 30:
ups[o] = round(A._sh((1 - W_BLEND) * J["B"] + W_BLEND * J["C"]) - A._sh(J["B"]), 3)
vals = np.array(list(ups.values()), float) if ups else np.array([np.nan])
band["uplift_per_offset"] = ups
band["uplift_canonical"] = ups.get(0)
band["uplift_median"] = round(float(np.median(vals)), 3)
band["uplift_frac_positive"] = round(float((vals > 0).mean()), 3)
return band
def delever_null(C: pd.Series, w: float = W_BLEND) -> dict:
"""NULL DEL DE-LEVERING (obbligatorio su ogni claim di meno drawdown, 5 occorrenze nel
progetto): esiste k<1 che, applicato al SOLO TP01, da' lo STESSO maxDD con Sharpe MIGLIORE?
Se si', l'overlay non serve: bastava meno leva."""
B = A.tp01_baseline_daily()
J = pd.concat({"B": B, "C": C}, axis=1, join="inner").dropna()
if len(J) < 60:
return dict(run=False, reason="overlap insufficiente")
blend = (1 - w) * J["B"] + w * J["C"]
dd_bl, sh_bl = A._dd_ret(blend), A._sh(blend)
dd_b, sh_b = A._dd_ret(J["B"]), A._sh(J["B"])
if dd_bl >= dd_b:
return dict(run=True, claims_less_dd=False, dd_blend=round(dd_bl, 4),
dd_tp01=round(dd_b, 4), sharpe_blend=round(sh_bl, 3),
sharpe_tp01=round(sh_b, 3),
note="nessun claim di DD: il blend NON riduce il drawdown -> null non pertinente")
ks = np.linspace(0.05, 1.0, 96)
dds = np.array([A._dd_ret(k * J["B"]) for k in ks])
k = float(ks[int(np.argmin(np.abs(dds - dd_bl)))])
sh_k = A._sh(k * J["B"])
return dict(run=True, claims_less_dd=True, dd_blend=round(dd_bl, 4), dd_tp01=round(dd_b, 4),
sharpe_blend=round(sh_bl, 3), k_equal_dd=round(k, 3),
dd_k=round(A._dd_ret(k * J["B"]), 4), sharpe_k_tp01=round(sh_k, 3),
passes=bool(sh_bl > sh_k),
note=("il blend batte il de-levering" if sh_bl > sh_k
else "REFUTED: k*TP01 da' lo stesso DD con Sharpe migliore"))
def smallcap_report(fn) -> dict:
out = {}
for a in A.CERTIFIED:
df = A.get(a, "1d")
sc = A.eval_weights_smallcap(df, A._call_target(fn, df, a), capital=600.0, min_order=5.0)
out[a] = dict(modeled=sc["modeled"]["sharpe"], realistic=sc["realistic"]["sharpe"],
haircut=sc["sharpe_haircut"], n_trades=sc["n_executed_trades"])
return out
def active_window_metrics(C: pd.Series) -> dict:
"""Trappola #9 (barre attive vs calendario): una serie che e' 0 per meta' della storia
(XDISP parte nel 2024) ha Sharpe DILUITO di sqrt(frazione attiva). Si riporta anche il
numero sulla sola finestra ATTIVA, senza usarlo per decidere."""
nz = C[C != 0.0]
if len(nz) < 30:
return dict(active_days=int(len(nz)))
first = nz.index[0]
sub = C[C.index >= first]
return dict(active_days=int(len(nz)), frac_active=round(float(len(nz) / max(1, len(C))), 3),
first_active=str(first.date()), sharpe_active_window=round(A._sh(sub), 3),
maxdd_active_window=round(A._dd_ret(sub), 4))
# ===========================================================================
# SANITY — un setaccio che non si controlla non sta setacciando niente
# ===========================================================================
def sanity() -> None:
print("### SANITY")
for a in A.CERTIFIED:
b0, g = bars_at(a, 0), A.get(a, "1d")
n = min(len(b0), len(g))
d = float(np.max(np.abs(b0["close"].values[:n] - g["close"].values[:n])))
dts = float(np.max(np.abs(b0["timestamp"].values[:n] - g["timestamp"].values[:n])))
print(f" bars_at({a},0) vs A.get({a},'1d'): n={n}/{len(g)} max|dclose|={d:.6g} max|dts|={dts:.0f}")
assert d < 1e-9 and dts == 0, "ricostruzione barre a offset 0 NON identica al feed certificato"
ts, dsp, nfiles = disp_aux()
ok = np.isfinite(dsp)
print(f" disp_aux: {nfiles} file HL, {int(ok.sum())} giorni con >=15 asset, "
f"da {pd.Timestamp(ts[ok][0], unit='ms', tz='UTC').date()} "
f"a {pd.Timestamp(ts[ok][-1], unit='ms', tz='UTC').date()}")
B = A.tp01_baseline_daily()
print(f" TP01 baseline: n={len(B)} ShFULL={A._sh(B):.3f} ShHOLD={A._sh(B[B.index >= A.HOLDOUT]):.3f}")
print()
# ===========================================================================
# MAIN
# ===========================================================================
def main() -> None:
t0 = time.time()
print(__doc__.split("USO:")[0])
sanity()
pooled_sh: list[float] = []
results = []
for fam in FAMILIES:
code, fac, grid = fam["code"], fam["factory"], fam["grid"]
t1 = time.time()
print(f"### {code} — {len(grid)} celle dichiarate")
sel = A.select_cell_insample(fac, grid, ("1d",))
pooled_sh.extend([s for s in sel["all_full_sharpe"] if np.isfinite(s)])
rep = A.study_family_honest(code, fac, grid, ("1d",))
ch = rep["chosen"]
if ch is None:
print(f" nessuna cella valida ({rep.get('reason')})\n")
results.append(dict(code=code, n_cells=len(grid), chosen=None,
killed_by="nessuna cella valutabile"))
continue
fn = fac(tf=ch["tf"], **ch["params"])
C = A.candidate_daily(fn, tf=ch["tf"])
m = rep["marginal"]["marginal"]
absr = rep["marginal"]["absolute"]["cells"][0]
caus = A.causality_ok(fn, tf="1d")
dayb = A.day_boundary_robust(fn, tf="1d") if fam["price_only"] else \
dict(verdict="NON GIRATO", reason="segnale con merge su serie ausiliaria: "
"spostare l'etichetta in avanti sposta il merge nel FUTURO (leak indotto), "
"non e' un controllo -> vale la banda d'ancora")
anch = anchor_report(fn)
deln = delever_null(C)
smal = smallcap_report(fn)
impl = A.implausible_sharpe(C)
actw = active_window_metrics(C)
full_sh = A._sh(C)
hold_sh = A._sh(C[C.index >= A.HOLDOUT])
mm = A._metrics_from_net(C.values, C.index)
results.append(dict(
code=code, n_cells=len(grid), chosen=ch, fn=fn, C=C,
full_sharpe=round(full_sh, 3), hold_sharpe=round(hold_sh, 3),
maxdd=mm["maxdd"], cagr=mm["cagr"],
corr=m.get("corr_full"), corr_hold=m.get("corr_hold"),
verdict=rep["marginal"]["marginal_verdict"],
earns_slot=rep["earns_slot_marginal"], honest=rep["earns_slot_honest"],
dsr_family=rep["deflated_sharpe"], dsr_pass=rep["dsr_pass"],
insample_sharpe=m.get("cand_insample_sharpe"),
has_insample_edge=m.get("has_insample_edge"),
robust_oos=m.get("robust_oos"), is_hedge=m.get("is_hedge"),
uplift_full=m["blends"]["w25"]["uplift_full"],
uplift_hold=m["blends"]["w25"]["uplift_hold"],
multicut=m.get("multicut_uplift"), abs_grade=rep["marginal"]["abs_grade"], _m=m,
fee_survives=absr.get("fee_survives"),
causality=caus, dayb=dayb, anchor=anch, delever=deln,
smallcap=smal, implausible=impl, active=actw,
))
print(f" cella IN-SAMPLE-ONLY: {ch['params']} (IS Sh {ch['insample_sharpe']}, "
f"rango 1/{len(sel['rows'])} in-sample; FULL Sh di quella cella {ch['full_sharpe']})")
print(f" standalone FULL {full_sh:+.3f} | HOLD {hold_sh:+.3f} | maxDD {mm['maxdd']:.1%} | "
f"CAGR {mm['cagr']:+.2%} | IS {m.get('cand_insample_sharpe')}")
print(f" marginale {rep['marginal']['marginal_verdict']:<9} corr->TP01 {m.get('corr_full')} "
f"(hold {m.get('corr_hold')}) uplift w25 full {m['blends']['w25']['uplift_full']:+.3f} "
f"hold {m['blends']['w25']['uplift_hold']}")
print(f" robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} "
f"is_hedge={m.get('is_hedge')} abs={rep['marginal']['abs_grade']} "
f"fee_survives={absr.get('fee_survives')}")
print(f" DSR famiglia {rep['deflated_sharpe']} (atteso null max {rep['expected_null_max']}) "
f"pass={rep['dsr_pass']} earns_slot_honest={rep['earns_slot_honest']}")
print(f" causality ok={caus['ok']} max_tail_diff={caus['max_tail_diff']}")
print(f" confine {dayb.get('verdict')} " +
(f"spread {dayb.get('spread')} per_offset {dayb.get('per_offset')}"
if fam["price_only"] else f"({dayb.get('reason','')[:60]}...)"))
_nan = float("nan")
print(f" ancora Sh canonica {anch.get('canonical', _nan):+.3f} "
f"(pctl {anch.get('canonical_pctl')}) "
f"MEDIANA {anch.get('median', _nan):+.3f} "
f"banda [{anch.get('lo', _nan):+.3f},{anch.get('hi', _nan):+.3f}] "
f"frac>0 {anch.get('frac_positive')}")
print(f" uplift canonico {anch.get('uplift_canonical')} MEDIANA "
f"{anch.get('uplift_median')} frac>0 {anch.get('uplift_frac_positive')}")
print(f" de-levering {deln.get('note')}")
print(f" $600 " + " ".join(
f"{a}: mod {v['modeled']:+.2f} -> real {v['realistic']:+.2f} (haircut {v['haircut']:+.2f}, "
f"{v['n_trades']} trade)" for a, v in smal.items()))
print(f" implausible {impl.get('implausible')} {impl.get('reasons')}")
if actw.get("frac_active", 1.0) < 0.9:
print(f" attive {actw}")
print(f" [{time.time() - t1:.1f}s]\n")
# ---- DSR DI SCREEN: i trial sono TUTTE le celle di TUTTE le famiglie -------------
print("### DSR DI SCREEN (il conto onesto per un setaccio: scelgo il meglio fra "
f"{len(pooled_sh)} celle, non fra 24)")
for r in results:
if r.get("chosen") is None:
continue
d, s0 = A.deflated_sharpe(r["full_sharpe"], pooled_sh, r["C"])
r["dsr_screen"] = round(d, 3) if np.isfinite(d) else None
print(f" {r['code']:<6} Sh {r['full_sharpe']:+.3f} DSR famiglia {r['dsr_family']} "
f"DSR screen {r['dsr_screen']} (null max atteso {s0:.3f})")
print()
deep_dive(results)
# ---- TABELLA FINALE --------------------------------------------------------------
def killer(r) -> str:
if r.get("chosen") is None:
return "nessuna cella valutabile"
why = []
if not r["causality"]["ok"]:
why.append("CAUSALITY")
if r["verdict"] != "ADDS":
why.append(f"marginal={r['verdict']}")
if not r["has_insample_edge"]:
why.append("no in-sample edge (<0.5)")
if not r["robust_oos"]:
why.append("robust_oos")
if r["is_hedge"]:
why.append("is_hedge")
if not r["dsr_pass"]:
why.append(f"DSR fam {r['dsr_family']}")
if r.get("dsr_screen") is not None and r["dsr_screen"] < 0.95:
why.append(f"DSR screen {r['dsr_screen']}")
if r["dayb"].get("verdict") == "ARTIFACT-RISK":
why.append("confine ARTIFACT-RISK")
um = r["anchor"].get("uplift_median")
if um is not None and np.isfinite(um) and um <= 0:
why.append("ancora: uplift mediano <=0")
if r["delever"].get("claims_less_dd") and not r["delever"].get("passes"):
why.append("null de-levering")
if r["implausible"].get("implausible"):
why.append("implausible_sharpe")
return " + ".join(why) if why else "-- nessun gate lo uccide --"
print("### TABELLA — ORTHO-SCREEN 2026-08-22 (candidato | trial | ShFULL | hold-out | "
"corr->TP01 | marginale | DSR fam / screen | esito)")
hdr = (f"{'cand':<7}{'trial':>6}{'ShFULL':>8}{'hold':>7}{'maxDD':>8}{'CAGR':>8}"
f"{'corr':>7} {'marginale':<10}{'DSRfam':>7}{'DSRscr':>8} esito / gate che l'ha ucciso")
print(hdr); print("-" * len(hdr))
for r in results:
if r.get("chosen") is None:
print(f"{r['code']:<7}{r['n_cells']:>6}{'--':>8}{'--':>7}{'--':>8}{'--':>8}{'--':>7} "
f"{'--':<10}{'--':>7}{'--':>8} SCARTATO — {killer(r)}")
continue
ok = (r["verdict"] == "ADDS" and r["dsr_pass"] and (r.get("dsr_screen") or 0) >= 0.95
and r["causality"]["ok"])
esito = "CANDIDATO" if ok else "SCARTATO"
print(f"{r['code']:<7}{r['n_cells']:>6}{r['full_sharpe']:>+8.3f}{r['hold_sharpe']:>+7.2f}"
f"{r['maxdd']:>8.1%}{r['cagr']:>+8.1%}{r['corr']:>7.2f} {r['verdict']:<10}"
f"{str(r['dsr_family']):>7}{str(r.get('dsr_screen')):>8} {esito} — {killer(r)}")
print()
print("### FAMIGLIE NON TESTATE (l'elenco di cio' che non ho guardato fa parte del risultato)")
for line in NOT_TESTED:
print(" - " + line)
print(f"\n[totale {time.time() - t0:.1f}s]")
def mk_mom(tf="1d", H=5, shape="lin"):
"""CONTROLLO per TACC: il LIVELLO del momentum a orizzonte H, stessa pipeline
(z-score 365g -> clip -> vol_target). Serve a rispondere alla domanda della famiglia:
l'ACCELERAZIONE aggiunge qualcosa al LIVELLO, o e' un momentum corto travestito?"""
def target(df, asset):
c = df["close"].values.astype(float)
n = len(c)
m = np.full(n, np.nan)
m[H:] = c[H:] / c[:-H] - 1.0
u = np.clip(roll_z(m, 365) / 2.0, -1.0, 1.0)
return _sized(_shape(u, shape), df)
return target
MOM_CONTROL_GRID = _grid(H=[5, 20, 60], shape=["lin", "sign"]) # 6 celle di CONTROLLO
def deep_dive(results) -> None:
"""APPROFONDIMENTO sull'unica famiglia arrivata a marginal=ADDS.
Due domande, entrambe misurate:
(a) QUALE sotto-gate di robust_oos ha fallito (clean-year? jackknife? multi-cut?);
(b) l'accelerazione e' distinta dal LIVELLO del momentum corto, o e' quello travestito?
-> 6 celle di CONTROLLO dichiarate (H x shape), che si SOMMANO ai 168 trial."""
adds = [r for r in results if r.get("chosen") is not None and r["verdict"] == "ADDS"]
if not adds:
print("### APPROFONDIMENTO — nessuna famiglia a marginal=ADDS, niente da approfondire\n")
return
r = max(adds, key=lambda x: x["uplift_hold"] or -9)
print(f"### APPROFONDIMENTO — {r['code']} (l'unica a marginal=ADDS)")
print(f" perche' robust_oos=False: clean_year_uplift={r['_m'].get('clean_year_uplift')} "
f"(serve >0.02) jackknife_min_uplift={r['_m'].get('jackknife_min_uplift')} (serve >0)")
print(f" multicut (uplift a ogni taglio d'anno): {r['multicut']}")
print(f" uplift TP01-up {r['_m'].get('uplift_tp01_up')} / TP01-down {r['_m'].get('uplift_tp01_down')} "
f"| hedge_yearly_corr {r['_m'].get('hedge_yearly_corr')} | alpha/anno {r['_m'].get('alpha_ann')}")
print(" --- accelerazione vs LIVELLO del momentum (6 celle di controllo) ---")
B = A.tp01_baseline_daily()
C = r["C"]
best = None
rows_ctl = []
for p in MOM_CONTROL_GRID:
fnm = mk_mom(tf="1d", **p)
Cm = A.candidate_daily(fnm, tf="1d")
J = pd.concat({"A": C, "M": Cm}, axis=1, join="inner").dropna()
JB = pd.concat({"B": B, "M": Cm}, axis=1, join="inner").dropna()
up = A._sh((1 - W_BLEND) * JB["B"] + W_BLEND * JB["M"]) - A._sh(JB["B"])
row = dict(params=p, sharpe=round(A._sh(Cm), 3),
insample=round(A._sh(Cm[Cm.index < A.HOLDOUT]), 3),
hold=round(A._sh(Cm[Cm.index >= A.HOLDOUT]), 3),
corr_to_acc=round(float(J["A"].corr(J["M"])), 3),
corr_to_tp01=round(float(JB["B"].corr(JB["M"])), 3),
uplift_w25_full=round(up, 3))
print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} IS {row['insample']:+.3f} "
f"hold {row['hold']:+.3f} corr->{r['code']} {row['corr_to_acc']:+.3f} "
f"corr->TP01 {row['corr_to_tp01']:+.3f} uplift {row['uplift_w25_full']:+.3f}")
rows_ctl.append(row)
if best is None or row["corr_to_acc"] > best["corr_to_acc"]:
best = row
print(f" livello piu' vicino all'accelerazione: {best['params']} corr {best['corr_to_acc']:+.3f}")
print(" lettura: corr alta col LIVELLO => l'accelerazione non e' una dimensione nuova; "
"corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.")
bh = max(rows_ctl, key=lambda x: x["hold"])
bi = max(rows_ctl, key=lambda x: x["insample"])
print(f" TRAPPOLA DICHIARATA: la cella di controllo migliore SULL'HOLD-OUT e' {bh['params']} "
f"(hold {bh['hold']:+.3f}); quella scelta IN-SAMPLE-ONLY e' {bi['params']} "
f"(IS {bi['insample']:+.3f}, hold {bi['hold']:+.3f}). Se coincidono, il controllo e'"
" selezionabile; se no, guardare la colonna hold e' selezione-sull'hold-out (trappola 3)"
" e NON promuove nulla.\n")
NOT_TESTED = [
"catena opzioni / vol term-structure: 3-5 mesi di storia -> non esiste hold-out, al massimo "
"un LEAD; e altri agenti dell'ondata ci stanno sopra.",
"funding (carry, time-series, cross-sectional): filone dichiarato CHIUSO su 3 lati.",
"calendario in ogni forma (weekend 375 trial, expiry, stagionalita', event-clock, "
"ora-del-giorno): famiglia dichiarata SATURA.",
"mean-reversion pura / fade: morta anche a fee zero. Qui rientra SOLO come branch dichiarato "
"di XTAIL e VPX, per misurarla dentro un conditioner nuovo.",
"microstruttura 5m/15m: morta per fee, e una sweep su 5m sfonda il budget macchina.",
"cross-sectional sui 51 alt HL: territorio XS01, e l'espansione d'universo e' gia' refutata. "
"Qui i 51 alt entrano solo come AGGREGATO time-series (XDISP).",
"equity / GTAA / MAT01: GTAA01 non e' deployabile (PRIIPs) e MAT01 e' refutato.",
"macro regime gate: ridondante col trend (corr 0.989).",
"CRT / ICT / Elliott / Fibonacci / Albimarini: 3 ondate di refutazione.",
"gamma scalping long-vol e overlay DD su VRP01 (5/5 refutati).",
"SOL come terza gamba: decisione dell'operatore del 22/08.",
]
if __name__ == "__main__":
main()
+975
View File
@@ -0,0 +1,975 @@
"""r0822_prop_alloc — PROGETTARE l'allocazione per la BARRIERA, non per lo Sharpe (2026-08-22).
IL BUCO CHE CHIUDE
------------------
Il canale prop/funded e' l'unico misurato che moltiplica il nozionale senza possedere capitale.
Il progetto lo ha gia' VALUTATO (24-25/07: `r0725_prop_ladder.py`, `r0725_prop_coupled.py`,
`r0725_hyro.py`) ma non lo ha mai **PROGETTATO**: ha sempre preso il libro ottimizzato per lo
Sharpe (config A = live 75/25, config B = 55/20/25 scelta a mano) e poi MISURATO P(pass) del
risultato. Due punti nello spazio dei pesi, tre leve, nessuna ottimizzazione, nessun null.
Qui la funzione obiettivo cambia: sotto una **barriera di maxDD STATICA per-conto** conta la coda
sinistra e la DURATA, non il rapporto rendimento/vol. Si ottimizza
J(w, k) = P(passare l'eval) x P(essere vivo dopo 12 mesi da funded)
su pesi w e leva k, con la lente WICK ACCOPPIATA (la `close-only` e' CIECA: 0% di breach da
daily-loss su ogni configurazione, la regola non scatta mai sulle chiusure).
LA PREVISIONE, REGISTRATA PRIMA DI MISURARE
-------------------------------------------
Nel limite gaussiano con leva LIBERA l'obiettivo si riduce allo Sharpe, e si vede in due righe.
Per un moto browniano con drift mu e vol sigma a leva k, la probabilita' di toccare +T prima di
-D vale P = (1-e^{-theta*D})/(1-e^{-theta*(T+D)}) con **theta = 2*mu/(k*sigma^2)**: la leva entra
SOLO dividendo theta. Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) si ottiene
theta ~ 2*mu^2/(T*sigma^2) = 2*S^2/T, con S = Sharpe: **la vol sparisce e resta lo Sharpe al
quadrato**. Quindi mi aspetto PRIMA di misurare:
(a) l'argmax di J vicino all'argmax di Sharpe -> la premessa "obiettivo diverso" e' in gran
parte FALSA sui PESI;
(b) il contenuto vero nella **LEVA**, che lo Sharpe non determina affatto, e nella deviazione
dal gaussiano (skew, wick, daily-loss: una soglia ASSOLUTA non e' scale-invariante);
(c) leva ottima per J **piu' bassa** di quella ottima per il payout (0.75x, 25/07): J e' una
probabilita' di sopravvivenza, il payout e' un flusso.
Se (a) risulta falsa, e' un risultato; se risulta vera, il filone "riallocare per la barriera"
si chiude e si smette di pagarlo.
CONVENZIONI DICHIARATE
----------------------
* Macchineria del wick ACCOPPIATO **riusata** da `r0725_prop_coupled.py` (import, non riscritta):
`_hourly_legs` (recon MTM orario TP01/SKH01), `crypto_daily_tuples` (minimo ESATTO sul path
orario condiviso), `xsec_daily_tuples` (XS01 dagli OHLC 1d, ordine condiviso avverso).
* VRP01 e GTAA01 sono AGGIUNTI qui (non c'erano): GTAA01 col wick dagli OHLC dei 6 ETF (long-only
co-moventi -> ogni gamba al proprio minimo = convenzione severa dichiarata); VRP01 con
**gap = 0**, cioe' CIECO, perche' il suo mark infra-settimana non e' nel dataset dello sleeve
(il rendimento settimanale e' lumpato sul giorno di scadenza). E' una lente GENEROSA per VRP01:
la misura del 30/07 sulle quote reali dice mark peggiore infra-settimana mediano -6.9% e minimo
**-81.7%** del capitale su un trade finito in UTILE. Se VRP01 perde sotto una lente generosa,
perde.
* de-luck sul DRIFT **x0.89** (fattore MISURATO il 26/07 sul libro live, `r0726_deluck_factor.py`),
non x0.6 come nel 25/07 (li' era stimato a occhio e ri-misurato troppo severo del 31-34%).
Sensibilita' a 1.00 e 0.60 girata: cio' che conta e' se l'ORDINE fra le configurazioni tiene.
* bootstrap a blocchi (20g) con **indici CONDIVISI fra tutte le configurazioni** -> le differenze
fra configurazioni sono APPAIATE e il loro errore MC e' molto minore di quello dei livelli.
* finestra comune **2024+** (XS01 nasce li'): ~2.6 anni, ed e' la finestra in cui XS01 e' stato
scoperto -> le configurazioni che lo contengono sono FAVORITE per costruzione. Dichiarato.
* fisco 33% sui payout, split 80%, regole vere delle due firm (max-loss STATICO dal saldo
iniziale, base ripristinata dopo il prelievo — bug del 25/07 gia' corretto a monte).
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822_prop_alloc.py`
"""
from __future__ import annotations
import sys
import time
from functools import lru_cache
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 src.data.eq_splits import repair_splits # noqa: E402
from src.portfolio.gtaa import ( # noqa: E402
ANN, EQ_UNIVERSE, GTAA_DEFAULT_CAPITAL, HORIZONS, IB_MIN_TRADE_USD, REBAL_BAND_USD,
REBAL_EVERY, TARGET_VOL, ib_commission,
)
from src.portfolio.portfolio import metrics, weights_tilt_null # noqa: E402
from src.portfolio.sleeves import ( # noqa: E402
GTAA_BOOK_ACTIVATION, _gtaa_daily_returns, _skyhook_returns, _tp01_returns,
_vrp_combo_returns, _xsec_returns,
)
import r0725_prop_coupled as pc # noqa: E402
import r0725_prop_ladder as pl # noqa: E402
from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402
RAW = ROOT / "data" / "raw"
# ------------------------------------------------------------------ costanti dichiarate
DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio
START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui)
BLOCK = 20 # blocco del bootstrap, in giorni
N_PATHS = 2500
SPLIT = pl.SPLIT # 80% al trader
TARGET_EUR_DAY = 50.0
FIRMS = {f["name"]: f for f in pl.FIRMS}
SLEEVES = ("TP01", "SKH01", "XS01", "VRP01", "GTAA01")
CRYPTO_PROP = ("TP01", "SKH01", "XS01") # cio' che una firm CRYPTO puo' davvero far eseguire
# ================================================================== §0. PANNELLO (R, wick)
@lru_cache(maxsize=8)
def _eq_ohlc(sym: str) -> pd.DataFrame:
p = RAW / f"eq_{sym.lower()}_1d.parquet"
d = pd.read_parquet(p)
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d = d.sort_index()
d, _ = repair_splits(d) # split IWM/EFA non aggiustati (25/07)
return d[["open", "high", "low", "close"]].astype(float)
def _gtaa_exposure(close: pd.Series) -> np.ndarray:
"""Copia FEDELE di gtaa._exposure (che prende una Series di soli close)."""
px = close.values
n = len(px)
tgt = np.zeros(n)
mh = max(HORIZONS)
for i in range(mh, n):
tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS])
s = pd.Series(tgt, index=close.index)
rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN
scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0)
return (s * scale).clip(0, 1.0).values
@lru_cache(maxsize=2)
def gtaa_daily_tuples() -> pd.DataFrame:
"""(R, m) giornalieri di GTAA01: chiusura replicata da gtaa.gtaa_returns e minimo intraday
dagli OHLC delle 6 gambe. Long-only e co-moventi -> convenzione **ogni gamba al proprio
minimo insieme** (severa, la stessa famiglia del bound 'perleg' di XS01).
Weekend/festivi: R = 0 e m = 0 (capitale IB fermo — convenzione del book)."""
cap_leg = GTAA_DEFAULT_CAPITAL / len(EQ_UNIVERSE)
legs_r, legs_m = {}, {}
for sym in EQ_UNIVERSE:
d = _eq_ohlc(sym)
close = d["close"]
ex = np.nan_to_num(_gtaa_exposure(close))
px = close.values
prev = np.concatenate(([np.nan], px[:-1]))
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
ret_low = d["low"].values / prev - 1.0 # long-only: il peggio e' il minimo
held = np.empty(len(ex))
comm = np.zeros(len(ex))
cur = 0.0
for i in range(len(ex)): # identico a gtaa._gated_returns
if i % REBAL_EVERY == 0:
notional = abs(ex[i] - cur) * cap_leg
if notional >= max(REBAL_BAND_USD, IB_MIN_TRADE_USD):
comm[i] = ib_commission(notional, px[i]) / cap_leg
cur = ex[i]
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1]
legs_r[sym] = pd.Series(pos * ret - comm, index=close.index)
legs_m[sym] = pd.Series(pos * np.nan_to_num(ret_low) - comm, index=close.index)
R = pd.concat(legs_r, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
M = pd.concat(legs_m, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
days = pd.date_range(R.index.min().normalize(), R.index.max().normalize(), freq="1D", tz="UTC")
R = R.reindex(days).fillna(0.0)
M = M.reindex(days).fillna(0.0)
out = pd.DataFrame({"R": R, "m": np.minimum(M, R)})
return out[out.index >= GTAA_BOOK_ACTIVATION]
@lru_cache(maxsize=2)
def vrp_daily_tuples() -> pd.DataFrame:
"""(R, m) di VRP01 con **m = R**: il mark infra-settimana NON e' nel dataset dello sleeve
(rendimento settimanale lumpato sul giorno di scadenza). Lente CIECA e GENEROSA — dichiarata."""
r = _vrp_combo_returns().dropna().sort_index()
if r.index.tz is None:
r.index = r.index.tz_localize("UTC")
return pd.DataFrame({"R": r.values, "m": r.values}, index=r.index)
def _d(s: pd.Series) -> pd.Series:
s = s.dropna().sort_index()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
_CRY_CACHE: dict = {}
def _crypto_stressed(w_tp: float, w_skh: float, m_tp: float, m_skh: float) -> pd.DataFrame:
"""Come `pc.crypto_daily_tuples` (minimo ESATTO sul path orario condiviso) ma con un taglio
del DRIFT applicato alla singola gamba PRIMA di comporre il path. Serve perche' il minimo
giornaliero non e' separabile per gamba: non si puo' tagliare il drift a valle."""
(J,) = pc._hourly_legs()
legs = {}
for nm, (rc, wc, mult) in {"tp": ("tp_r", "tp_w", m_tp), "sk": ("skh_r", "skh_w", m_skh)}.items():
r = J[rc].values.copy()
w = J[wc].values.copy()
if mult != 1.0:
cut = (1.0 - mult) * r.mean()
r = r - cut
w = w - cut
legs[nm] = (r, w)
r_h = w_tp * legs["tp"][0] + w_skh * legs["sk"][0]
w_h = w_tp * legs["tp"][1] + w_skh * legs["sk"][1]
idx = J.index
days = idx.floor("D")
rows = []
for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0):
sl = pos.values
r = r_h[sl]
wk = w_h[sl]
cum = np.cumprod(1.0 + r)
cum_prev = np.concatenate(([1.0], cum[:-1]))
m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0
rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0)))
return pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day")
def _crypto(w_tp: float, w_skh: float, m_tp: float = 1.0, m_skh: float = 1.0) -> pd.DataFrame:
key = (round(w_tp, 6), round(w_skh, 6), round(m_tp, 4), round(m_skh, 4))
if key not in _CRY_CACHE:
_CRY_CACHE[key] = (pc.crypto_daily_tuples(w_tp, w_skh) if (m_tp == 1.0 and m_skh == 1.0)
else _crypto_stressed(w_tp, w_skh, m_tp, m_skh))
return _CRY_CACHE[key]
@lru_cache(maxsize=4)
def _panel_index(start: str) -> pd.DatetimeIndex:
"""Griglia comune ai 5 sleeve (intersezione), dal `start` in poi."""
idx = _crypto(1.0, 0.0).index
for f in (pc.xsec_daily_tuples(), vrp_daily_tuples(), gtaa_daily_tuples()):
idx = idx.intersection(f.index)
return idx[idx >= pd.Timestamp(start, tz="UTC")]
_RG_CACHE: dict = {}
def book_RG(w: dict, start: str = START_WIN,
drift_mult: tuple = ()) -> tuple[np.ndarray, np.ndarray]:
"""(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve.
Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled).
XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07).
`drift_mult` = (("XS01", 0.5), ...): taglia il DRIFT di quello sleeve (non la vol) — serve
per lo stress giudiziale su uno sleeve scoperto DENTRO la finestra di misura."""
dm = dict(drift_mult)
key = (start, drift_mult) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES)
if key in _RG_CACHE:
return _RG_CACHE[key]
idx = _panel_index(start)
w_tp, w_skh = w.get("TP01", 0.0), w.get("SKH01", 0.0)
R = np.zeros(len(idx))
M = np.zeros(len(idx))
if w_tp > 0 or w_skh > 0:
c = _crypto(w_tp, w_skh, dm.get("TP01", 1.0), dm.get("SKH01", 1.0)).reindex(idx)
R += c["R"].values
M += c["m"].values
for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()),
("GTAA01", gtaa_daily_tuples())):
wt = w.get(nm, 0.0)
if wt > 0:
f = fr.reindex(idx)
rv = np.nan_to_num(f["R"].values)
mv = np.nan_to_num(f["m"].values)
mult = dm.get(nm, 1.0)
if mult != 1.0: # taglia il drift, lascia la forma della coda
cut = (1.0 - mult) * rv.mean() # NB: media PRIMA del taglio
rv = rv - cut
mv = mv - cut
R += wt * rv
M += wt * mv
R = np.nan_to_num(R)
M = np.nan_to_num(M)
_RG_CACHE[key] = (R, np.minimum(M, R) - R)
return _RG_CACHE[key]
def deluck(R: np.ndarray, factor: float = DELUCK) -> np.ndarray:
return R - (1.0 - factor) * R.mean()
# ================================================================== §1. SIMULATORE
def boot_idx(n_hist: int, n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray:
rng = np.random.default_rng(seed)
n_blocks = int(np.ceil(n_days / block))
st = rng.integers(0, n_hist - block, size=(n_paths, n_blocks))
return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
def eval_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str,
lens: str = "coupled") -> dict:
"""P(passare l'eval) entro l'orizzonte del bootstrap + giorni al pass."""
tgt, dd, dl = FIRMS[firm]["ev"]
r = R[idx] * lev
g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r)
P, D = r.shape
eq = np.ones(P)
alive = np.ones(P, bool)
passed = np.zeros(P, bool)
tpass = np.full(P, D, int)
for t in range(D):
act = alive & ~passed
if not act.any():
break
rt, gt = r[:, t], g[:, t]
eqp = eq
eq = np.where(act, eq * (1.0 + rt), eq)
eq_low = np.where(act, eqp * (1.0 + rt + gt), eq)
bust = act & ((eq_low < 1.0 - dd) | (rt + gt < -dl))
alive &= ~bust
ok = act & ~bust & (eq >= 1.0 + tgt)
tpass[ok] = t
passed |= ok
return dict(p_pass=float(passed.mean()), passed=passed,
t_pass=float(np.median(tpass[passed])) if passed.any() else np.nan)
def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str,
notional: float, lens: str = "coupled") -> dict:
"""P(vivo a fine orizzonte) + payout netto cumulato (prelievo mensile, base ripristinata)."""
ml, dl = FIRMS[firm]["fu"]
r = R[idx] * lev
g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r)
P, D = r.shape
eq = np.ones(P)
alive = np.ones(P, bool)
payout = np.zeros(P)
alive_at = {}
for t in range(D):
rt, gt = r[:, t], g[:, t]
eqp = eq
eq = np.where(alive, eq * (1.0 + rt), eq)
eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq)
bust = alive & ((eq_low < 1.0 - ml) | (rt + gt < -dl))
alive &= ~bust
if (t + 1) % 30 == 0:
take = alive & (eq > 1.0)
payout += np.where(take, (eq - 1.0) * notional, 0.0) * SPLIT * (1.0 - TAX_RATE)
eq = np.where(take, 1.0, eq)
if (t + 1) % 365 == 0:
alive_at[(t + 1) // 365] = float(alive.mean())
return dict(p_alive=float(alive.mean()), alive_at=alive_at,
e_payout=float(payout.mean()), payout=payout, alive=alive)
def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
lens: str = "coupled", factor: float = DELUCK,
notional: float = 100_000.0, drift_mult: tuple = ()) -> dict:
R, G = book_RG(w, drift_mult=drift_mult)
R = deluck(R, factor)
e = eval_sim(R, G, ev_idx, lev, firm, lens)
f = funded_sim(R, G, fu_idx, lev, firm, notional, lens)
ann = float(R.mean() * 365.0)
vol = float(R.std() * np.sqrt(365.0))
# stimatore per-percorso di J: eval e funded girano su indici di bootstrap INDIPENDENTI,
# quindi mean(passed_i * alive_i) stima p_pass*p_alive ed e' APPAIABILE fra configurazioni.
jvec = (e["passed"] & f["alive"]).astype(float)
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0),
vol=vol, drift=ann, t_pass=e["t_pass"], jvec=jvec)
def paired_delta(wA: dict, levA: float, wB: dict, levB: float, firm: str,
ev_idx: np.ndarray, fu_idx: np.ndarray,
drift_mult: tuple = ()) -> tuple[float, float]:
"""Differenza APPAIATA di J fra due configurazioni (stessi indici di bootstrap) + errore
standard della differenza. Il livello di J ha SE ~0.01; la DIFFERENZA molto meno.
⚠ `drift_mult` va passato: dimenticarlo confronta due configurazioni NON stressate e
stampa un delta che non c'entra con la riga in cui compare (errore commesso in sessione)."""
a = objective(wA, levA, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"]
b = objective(wB, levB, firm, ev_idx, fu_idx, drift_mult=drift_mult)["jvec"]
d = a - b
return float(d.mean()), float(d.std(ddof=1) / np.sqrt(len(d)))
# ================================================================== §2. IL CONTO ANALITICO
def p_hit_target_first(mu: float, sig: float, lev: float, T: float, D: float) -> float:
"""Gambler's ruin per moto browniano con drift: P(toccare +T prima di -D), SENZA limite di
tempo. theta = 2*mu/(k*sigma^2): la leva entra SOLO dividendo theta -> con leva -> inf la
probabilita' tende a D/(T+D) (37.5% con 10%/6%), qualunque sia lo Sharpe."""
if sig <= 0:
return float(mu > 0)
theta = 2.0 * mu / (lev * sig * sig)
if abs(theta) < 1e-12:
return D / (T + D)
return float((1.0 - np.exp(-theta * D)) / (1.0 - np.exp(-theta * (T + D))))
# ================================================================== §3. GRIGLIA / NULL
def simplex_grid(names: tuple, step_den: int) -> list[dict]:
"""Tutti i vettori di peso a passo 1/step_den sul simplesso di `names` (somma 1)."""
out = []
n = len(names)
def rec(prefix, rem, k):
if k == n - 1:
out.append(dict(zip(names, [p / step_den for p in prefix] + [rem / step_den])))
return
for v in range(rem + 1):
rec(prefix + [v], rem - v, k + 1)
rec([], step_den, 0)
return out
def wkey(w: dict) -> str:
return "/".join(f"{int(round(100 * w.get(nm, 0.0))):d}" for nm in CRYPTO_PROP)
def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
lens: str = "coupled", factor: float = DELUCK) -> pd.DataFrame:
rows = []
for w in grid:
for lev in levs:
o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor)
rows.append(dict(w=wkey(w), lev=lev,
**{k: v for k, v in o.items() if k != "jvec"}))
return pd.DataFrame(rows)
def random_weights(n: int, names: tuple, seed: int, floor: float = 0.0,
caps: dict | None = None) -> list[dict]:
"""Stesso campionatore di `portfolio.weights_tilt_null` (dirichlet uniforme sul simplesso +
rejection su floor/caps), ma qui serve a costruire il NULL della funzione obiettivo J."""
rng = np.random.default_rng(seed)
caps_v = np.array([(caps or {}).get(nm, 1.0) for nm in names], float)
out = []
while len(out) < n:
b = rng.dirichlet(np.ones(len(names)), size=4 * n + 64)
ok = (b >= floor).all(axis=1) & (b <= caps_v).all(axis=1)
out.extend(b[ok])
return [dict(zip(names, v)) for v in out[:n]]
# ================================================================== main
def hr(t: str = "") -> None:
print("\n" + "-" * 104)
if t:
print(f" {t}")
print("-" * 104)
def main() -> None:
try:
sys.stdout.reconfigure(line_buffering=True)
except Exception: # pragma: no cover
pass
t0 = time.time()
def tick(lab: str) -> None:
print(f" [{time.time()-t0:6.0f}s] {lab}")
print("=" * 104)
print(" PROP-ALLOC — allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe")
print("=" * 104)
# ---------------------------------------------------------------- A. pannello + sanity
idx = _panel_index(START_WIN)
print(f"\n finestra comune ai 5 sleeve: {len(idx)} giorni {idx[0].date()} -> {idx[-1].date()}"
f" (XS01 nasce nel 2024: e' anche la sua finestra di SCOPERTA -> lo favorisce)")
print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, "
f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi")
tick("pannello costruito")
off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()),
"VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())}
rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"],
"XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"],
"GTAA01": gtaa_daily_tuples()["R"]}
print("\n sanity — ricostruzione vs sleeve ufficiale. XS01/VRP01/GTAA01 devono coincidere")
print(" BARRA PER BARRA; TP01/SKH01 no per costruzione (il recon e' un mark-to-market ORARIO,")
print(" lo sleeve marca a fine-trade) -> per loro il controllo giusto e' il log-totale.")
print(f" {'sleeve':>8} {'max|delta| barra':>18} {'log-tot recon':>15} "
f"{'log-tot sleeve':>16} {'scarto':>9}")
for nm in SLEEVES:
com = off[nm].index.intersection(rec[nm].index)
a = off[nm].reindex(com).values
b = rec[nm].reindex(com).values
dmax = float(np.abs(a - b).max())
la, lb = float(np.log1p(a).sum()), float(np.log1p(b).sum())
flag = "OK" if dmax < 1e-9 else ("lente MTM" if nm in ("TP01", "SKH01") else "DIVERGE")
print(f" {nm:>8} {dmax:>18.2e} {lb:>15.4f} {la:>16.4f} {lb-la:>+9.4f} {flag}")
hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA — cio' che conta non e' lo Sharpe, e si vede qui")
print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} "
f"{'giorno peggiore':>16} {'gap p90':>9} {'gap peggiore':>13} {'g/vol':>7}")
prof = {}
for nm in SLEEVES:
R, G = book_RG({nm: 1.0})
Rd = deluck(R)
vol = Rd.std() * np.sqrt(365.0)
dr = Rd.mean() * 365.0
sk = float(pd.Series(Rd).skew())
prof[nm] = dict(sharpe=dr / vol if vol > 0 else 0.0, vol=vol, drift=dr, skew=sk,
worst=Rd.min(), gap90=np.percentile(-G, 90), gapw=G.min())
print(f" {nm:>8} {prof[nm]['sharpe']:>7.2f} {vol:>6.1%} {dr:>8.1%} {sk:>7.2f} "
f"{Rd.min():>15.2%} {-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp "
f"{(-G.min()/vol) if vol > 0 else 0:>7.2f}")
print("\n lettura: `gap peggiore` e' l'escursione intraday che la lente close-only NON vede;")
print(" `g/vol` la misura in unita' di vol annua. VRP01 ha gap 0 **per cecita' del dato**,")
print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,")
print(" minimo -81.7% del capitale su un trade finito in UTILE).")
tick("A. profilo sleeve")
hr("B. IL CONTO ANALITICO — perche' con leva LIBERA la barriera premia lo SHARPE, non la vol")
tgt, dd, _dl = FIRMS["HYRO"]["ev"]
print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, "
f"theta = 2*mu/(k*sigma^2):")
print(f" {'sleeve':>8} {'Sharpe':>7} | " + " ".join(f"{f'k={k:g}':>8}" for k in (0.25, 0.5, 1.0, 2.0, 4.0)))
for nm in SLEEVES:
p = prof[nm]
cells = " ".join(f"{p_hit_target_first(p['drift'], p['vol'], k, tgt, dd):>8.1%}"
for k in (0.25, 0.5, 1.0, 2.0, 4.0))
print(f" {nm:>8} {p['sharpe']:>7.2f} | {cells}")
print(f" {'limite k->inf':>16} | " + " ".join([f"{dd/(tgt+dd):>8.1%}"] * 5)
+ " <- D/(T+D): la leva alta CANCELLA l'edge")
print("\n Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) resta theta ~ 2*S^2/T:")
print(" la VOL sparisce e conta lo Sharpe AL QUADRATO. Percio' la previsione registrata prima")
print(" di misurare e': l'argmax di J sui PESI ~ l'argmax di Sharpe, e il contenuto vero e'")
print(" nella LEVA (che lo Sharpe non determina) e nella deviazione dal gaussiano.")
# ---------------------------------------------------------------- C. griglia
n_hist = len(idx)
EV_H, FU_H = 365, 365
ev_idx = boot_idx(n_hist, EV_H, N_PATHS, seed=8220)
fu_idx = boot_idx(n_hist, FU_H, N_PATHS, seed=8221)
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
STEP = 8
grid = simplex_grid(CRYPTO_PROP, STEP)
hr(f"C. GRIGLIA — {len(grid)} vettori di peso (passo 1/{STEP}) x {len(LEVS)} leve "
f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)")
print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. "
f"J = P(pass) x P(vivo 12m).")
tick("B. conto analitico")
t = time.time()
G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx)
print(f" ({time.time()-t:.0f}s)")
best = G3.loc[G3["J"].idxmax()]
# argmax di SHARPE: lo Sharpe non dipende dalla leva -> si sceglie il peso, poi la leva
# migliore PER QUEL PESO secondo J (altrimenti non e' un confronto onesto).
w_sh = G3.loc[G3["sharpe"].idxmax(), "w"]
sub_sh = G3[G3["w"] == w_sh]
best_sh = sub_sh.loc[sub_sh["J"].idxmax()]
live = G3[(G3["w"] == "75/25/0")]
best_live = live.loc[live["J"].idxmax()]
print(f"\n {'':>22} {'pesi TP/SKH/XS':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} "
f"{'P(vivo12m)':>11} {'Sharpe':>7} {'vol':>6} {'E[pay/a]':>10} {'g.al pass':>10}")
for lab, row in (("argmax J", best), ("argmax Sharpe", best_sh),
("libro LIVE 75/25", best_live)):
print(f" {lab:>22} {row['w']:>16} {row['lev']:>6.3f} {row['J']:>7.3f} "
f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} "
f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}")
dA, seA = paired_delta(w_argmax_dict := dict(zip(CRYPTO_PROP, [int(x)/100 for x in best["w"].split("/")])),
float(best["lev"]),
dict(zip(CRYPTO_PROP, [int(x)/100 for x in best_sh["w"].split("/")])),
float(best_sh["lev"]), "HYRO", ev_idx, fu_idx)
dB, seB = paired_delta(w_argmax_dict, float(best["lev"]),
{"TP01": 0.75, "SKH01": 0.25}, float(best_live["lev"]),
"HYRO", ev_idx, fu_idx)
print(f"\n DIFFERENZE APPAIATE (stessi indici di bootstrap; un livello di J ha SE ~0.01, "
f"una differenza molto meno):")
print(f" argmax J - argmax SHARPE : {dA:+.4f} +- {seA:.4f} "
f"-> {'INDISTINGUIBILI' if abs(dA) < 2*seA else 'differenza reale'}")
print(f" argmax J - libro LIVE : {dB:+.4f} +- {seB:.4f} "
f"-> {'INDISTINGUIBILI' if abs(dB) < 2*seB else 'differenza reale'}")
quota = abs(dA) / abs(dB) if dB != 0 else float("nan")
print(f" La prima riga E' il risultato del filone: riallocare per la BARRIERA invece che")
print(f" per lo SHARPE vale {dA:+.3f} di J, cioe' il {quota:.0%} del {dB:+.3f} che vale")
print(f" passare dal libro LIVE a un libro diversificato QUALSIASI. Il grosso del guadagno")
print(f" NON e' nella funzione obiettivo: e' nell'aggiungere lo sleeve che a $600 non entra.")
print("\n Le prime 8 celle per J:")
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
f"{'Sharpe':>7} {'E[pay/a]':>10}")
for i, (_, r) in enumerate(G3.sort_values("J", ascending=False).head(8).iterrows(), 1):
print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['J']:>7.3f} {r['p_pass']:>8.1%} "
f"{r['p_alive']:>8.1%} {r['sharpe']:>7.2f} {r['e_payout']:>9,.0f}$")
print("\n E per E[payout] (obiettivo DIVERSO: un flusso, non una sopravvivenza):")
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'E[pay/a]':>10} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8}")
for i, (_, r) in enumerate(G3.sort_values("e_payout", ascending=False).head(4).iterrows(), 1):
print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['e_payout']:>9,.0f}$ {r['J']:>7.3f} "
f"{r['p_pass']:>8.1%} {r['p_alive']:>8.1%}")
hr("C-bis. LA LEVA — la variabile che lo Sharpe non determina e che decide tutto")
print(f" pesi = argmax J ({best['w']}):")
print(f" {'leva':>7} {'J':>7} {'P(pass) 365g':>13} {'P(pass) 1095g':>14} {'P(vivo 12m)':>12} "
f"{'E[pay/a]':>10}")
ev_long = boot_idx(n_hist, 1095, N_PATHS, seed=8222)
w_best = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in best["w"].split("/")]))
Rb, Gb = book_RG(w_best)
Rb = deluck(Rb)
for lev in LEVS:
o = objective(w_best, lev, "HYRO", ev_idx, fu_idx)
pl_long = eval_sim(Rb, Gb, ev_long, lev, "HYRO")["p_pass"]
print(f" {lev:>7.3f} {o['J']:>7.3f} {o['p_pass']:>12.1%} {pl_long:>13.1%} "
f"{o['p_alive']:>11.1%} {o['e_payout']:>9,.0f}$")
print("\n A 365 giorni P(pass) e' a CAMPANA nella leva (a leva bassa il drift non arriva a +10%")
print(" in tempo); senza limite di tempo — che e' la regola vera di HyroTrader — e' MONOTONA")
print(" DECRESCENTE, come dice il conto analitico. Le due colonne dicono cose diverse e vanno")
print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.")
# ---------------------------------------------------------------- D. i null
tick("C. griglia + leva")
hr("D. NULL 1 — l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?")
rw = random_weights(200, CRYPTO_PROP, seed=8223)
lev_star = float(best["lev"])
Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw])
pctl = float((Jr < best["J"]).mean() * 100.0)
bestofk = 100.0 * len(grid) / (len(grid) + 1)
print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}")
print(f" null: {len(rw)} vettori di peso casuali uniformi sul simplesso (stesso campionatore di")
print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva")
print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | "
f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}")
print(f" percentile dell'argmax fra i casuali = {pctl:.1f}° contro la firma best-of-"
f"{len(grid)} = {bestofk:.1f}°")
print(f" ⚠ RISOLUZIONE: con {len(rw)} estrazioni un percentile vale a passi di "
f"{100/len(rw):.1f} punti -> {pctl:.1f}° e {bestofk:.1f}° NON sono distinguibili.")
print(f" E il fatto che decide: il MASSIMO dei {len(rw)} pesi casuali e' {Jr.max():.3f} contro"
f" {best['J']:.3f} dell'argmax")
print(f" della griglia -> un vettore estratto a caso lo {'EGUAGLIA' if Jr.max() >= best['J'] - 1e-9 else 'non lo raggiunge'}."
f" NESSUNA evidenza che quel vettore di pesi sia speciale:")
print(" cio' che conta non e' QUALE punto del simplesso, ma STARE nella regione diversificata.")
print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal "
f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)")
tick("D. null casuale")
hr("D-bis. NULL 2 — selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)")
half = n_hist // 2
R_all = {}
for w in grid:
R_, G_ = book_RG(w)
R_all[wkey(w)] = (deluck(R_), G_)
ev_h = boot_idx(half, EV_H, N_PATHS, seed=8224)
fu_h = boot_idx(half, FU_H, N_PATHS, seed=8225)
def _J(key, lev, sl):
R_, G_ = R_all[key]
e = eval_sim(R_[sl], G_[sl], ev_h, lev, "HYRO")["p_pass"]
f = funded_sim(R_[sl], G_[sl], fu_h, lev, "HYRO", 100_000.0)["p_alive"]
return e * f
IS, OOS = slice(0, half), slice(half, n_hist)
rows = []
for w in grid:
k = wkey(w)
for lev in LEVS:
rows.append((k, lev, _J(k, lev, IS), _J(k, lev, OOS)))
S = pd.DataFrame(rows, columns=["w", "lev", "J_is", "J_oos"])
pick = S.loc[S["J_is"].idxmax()]
oracle = S.loc[S["J_oos"].idxmax()]
live_oos = S[(S["w"] == "75/25/0")].sort_values("J_oos", ascending=False).iloc[0]
print(f" prima meta' {idx[0].date()}->{idx[half-1].date()} | seconda {idx[half].date()}->{idx[-1].date()}")
print(f" {'':>28} {'pesi':>16} {'leva':>6} {'J in-sample':>12} {'J out-of-sample':>16}")
print(f" {'scelto SUL SOLO in-sample':>28} {pick['w']:>16} {pick['lev']:>6.3f} "
f"{pick['J_is']:>12.3f} {S[(S['w']==pick['w'])&(S['lev']==pick['lev'])]['J_oos'].iloc[0]:>16.3f}")
print(f" {'oracolo (max sull OOS)':>28} {oracle['w']:>16} {oracle['lev']:>6.3f} "
f"{oracle['J_is']:>12.3f} {oracle['J_oos']:>16.3f}")
print(f" {'libro LIVE 75/25 (baseline)':>28} {live_oos['w']:>16} {live_oos['lev']:>6.3f} "
f"{live_oos['J_is']:>12.3f} {live_oos['J_oos']:>16.3f}")
rho = float(S["J_is"].corr(S["J_oos"], method="spearman"))
print(f" Spearman(J in-sample, J out-of-sample) su {len(S)} celle = {rho:+.2f}")
print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax")
print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.")
tick("D-bis. selezione in-sample")
hr("D-ter. NULL DEL DE-LEVERING — la leva e' GIA' dentro l'ottimizzazione, ma va detto")
print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'")
print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere")
print(" solo perche' e' meno esposta. La domanda che resta e': la MIGLIOR leva del libro LIVE")
print(" raggiunge il J dell'argmax?")
print(f" argmax J : {best['w']:>10} @ {best['lev']:g}x -> J = {best['J']:.3f}")
print(f" LIVE 75/25 al suo : {best_live['w']:>10} @ {best_live['lev']:g}x -> J = {best_live['J']:.3f}")
dJ = best["J"] - best_live["J"]
se = np.sqrt(0.25 / N_PATHS) * 2
print(f" delta = {dJ:+.3f} (errore MC su livelli ~+-{se:.3f}; le due celle condividono")
print(f" gli indici di bootstrap -> il delta e' APPAIATO e il suo errore e' piu' piccolo)")
# ---------------------------------------------------------------- E. banda d'ancora
hr("E. BANDA D'ANCORA — non ri-misurata sotto questa lente (costo); si usa il FATTORE misurato")
print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore")
print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se")
print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata")
print(" troppo severa) - 1.00 (nessuna correzione). L'argmax e' ri-cercato sulle 12 celle-peso")
print(" migliori (non su tutte le 45: costo), il che e' conservativo per la STABILITA'.")
cands = [best["w"], best_sh["w"], best_live["w"]]
top_keys = list(G3.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(12))
top_w = [dict(zip(CRYPTO_PROP, [int(x) / 100 for x in k.split("/")])) for k in top_keys]
print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands))
+ f" {'argmax pesi':>14} {'argmax leva':>11}")
for fct in (1.00, 0.89, 0.60):
cells = []
for c in dict.fromkeys(cands):
wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")]))
sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS]
cells.append(max(sub))
g = grid_scan(top_w, LEVS, "HYRO", ev_idx, fu_idx, factor=fct)
b = g.loc[g["J"].idxmax()]
print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells)
+ f" {b['w']:>14} {b['lev']:>11.3f}")
hr("E-bis. LO STRESS CHE DECIDE — XS01 e' stato SCOPERTO in questa finestra")
print(" L'argmax mette il 50% su XS01, il cui unico campione (2024+) e' anche quello su cui e'")
print(" stato scoperto e affinato (blend di lookback + gate di dispersione). CLAUDE.md lo")
print(" dichiara: 'la TAGLIA del guadagno e' ottimista'. Qui si taglia il DRIFT di XS01 (non la")
print(" vol, non la coda) e si guarda se la conclusione operativa sopravvive.")
print(f"\n {'drift XS01':>12} {'config':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} "
f"{'P(vivo)':>8} {'E[pay/a]':>10}")
for mult in (1.00, 0.75, 0.50, 0.25, 0.00):
dmt = (("XS01", mult),)
rows = []
for w in top_w:
for lv in LEVS:
rows.append((w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt)))
w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"])
lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"])
dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO", ev_idx,
fu_idx, drift_mult=dmt)
print(f" {mult:>12.2f} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {o_['p_pass']:>8.1%} "
f"{o_['p_alive']:>8.1%} {o_['e_payout']:>9,.0f}$ LIVE {oL['J']:.3f} "
f"delta {dd_:+.3f} +-{sd_:.3f}")
print("\n La colonna che decide e' l'ULTIMA. Se il vantaggio svanisce col drift di XS01, il")
print(" risultato e' un'ipotesi sul RENDIMENTO ATTESO di XS01 misurato sulla sua finestra di")
print(" scoperta; se resta a drift 0, il vantaggio e' pura DECORRELAZIONE e non dipende da")
print(" quell'ipotesi. Il numero, non la narrazione, dice quale delle due.")
hr("E-quater. LA DISTORSIONE CHE MI RIGUARDA — l'ottimo si appoggia a SKH01, e SKH01 e' lo")
print(" sleeve con la FORTUNA D'ANCORA piu' grande da restituire. Il LOO del 26/07: l'ancora")
print(" canonica gli regala 2/3 del contributo FULL (+0.43 -> +0.14) e il 70% dell'hold-out;")
print(" l'audit 02/07 lo trova al 93-98° pctl di 23 offset, 'spike, non plateau'. Questa lente")
print(" gira sull'ancora CANONICA e la banda non e' ri-misurabile qui -> il mio ottimo e'")
print(" distorto A FAVORE di SKH01 e va detto PRIMA di leggerlo. Taglio del drift di SKH01:")
print(" Le ultime due righe sono il taglio CONGIUNTO su SKH01 E XS01 (i due sleeve che il")
print(" progetto misura come piu' fortunati / piu' recenti): e' il caso peggiore onesto.")
print(f"\n {'taglio drift':>21} {'config':>16} {'leva':>6} {'J':>7} {'J del LIVE':>11} "
f"{'delta appaiato':>17}")
for dmt in ((("SKH01", 1.00),), (("SKH01", 0.66),), (("SKH01", 0.50),), (("SKH01", 0.33),),
(("SKH01", 0.50), ("XS01", 0.50)), (("SKH01", 0.33), ("XS01", 0.00))):
mult = dict(dmt)
rows = [(w, lv, objective(w, lv, "HYRO", ev_idx, fu_idx, drift_mult=dmt))
for w in top_w for lv in LEVS]
w_, lv_, o_ = max(rows, key=lambda x: x[2]["J"])
lvL, oL = max(((lv, objective({"TP01": 0.75, "SKH01": 0.25}, lv, "HYRO", ev_idx, fu_idx,
drift_mult=dmt)) for lv in LEVS), key=lambda x: x[1]["J"])
dd_, sd_ = paired_delta(w_, lv_, {"TP01": 0.75, "SKH01": 0.25}, lvL, "HYRO",
ev_idx, fu_idx, drift_mult=dmt)
lab = " ".join(f"{k.replace('01','')} x{v:.2f}" for k, v in mult.items())
assert abs(dd_ - (o_["J"] - oL["J"])) < 0.05, ( # guardia contro l'errore di sopra
f"delta appaiato {dd_:+.3f} incoerente con J {o_['J']:.3f} - {oL['J']:.3f}")
print(f" {lab:>21} {wkey(w_):>16} {lv_:>6.3f} {o_['J']:>7.3f} {oL['J']:>11.3f} "
f"{dd_:>+11.3f} +-{sd_:.3f}")
print(" NB: il taglio si applica anche al LIVE 75/25 (che ha SKH01 al 25%), quindi il delta e'")
print(" un confronto onesto: entrambe le colonne perdono, la domanda e' quale perde di piu'.")
hr("E-ter. IL BUCO CHE LA FINESTRA NON PUO' CHIUDERE — 2024-2026 non contiene un CRASH")
print(" L'ottimizzazione toglie peso a TP01 (Sharpe 0.79 su questa finestra). Ma TP01 e' lo")
print(" sleeve DIFENSIVO: il LOO del 26/07 misura il suo contributo hold-out NEGATIVO nel 99.1%")
print(" delle ancore e conclude 'uno sleeve difensivo si giudica sul SINISTRO, non sul premio'.")
print(" Su un conto funded la barriera morde PROPRIO nel sinistro. Qui la coppia crypto (che ha")
print(" storia dal 2019, quindi il 2022 dentro) e' ri-ottimizzata sulle due finestre:")
cry_full = pc.crypto_daily_tuples(1.0, 0.0).index
W2 = [{"TP01": i / 8, "SKH01": 1 - i / 8} for i in range(9)]
print(f"\n {'finestra':>26} {'giorni':>7} {'argmax TP/SKH':>15} {'leva':>6} {'J':>7} "
f"{'J del 75/25':>12} {'Sharpe argmax':>14}")
for st, lab in (("2019-03-14", "FULL 2019+ (col 2022)"), (START_WIN, "2024+ (senza crash)")):
pairs = {}
for w in W2:
c = pc.crypto_daily_tuples(w["TP01"], w["SKH01"])
c = c[c.index >= pd.Timestamp(st, tz="UTC")]
pairs[wkey(w)] = (deluck(c["R"].values), np.minimum(c["m"].values, c["R"].values) - c["R"].values)
nh = len(next(iter(pairs.values()))[0])
ev2 = boot_idx(nh, EV_H, N_PATHS, seed=8230)
fu2 = boot_idx(nh, FU_H, N_PATHS, seed=8231)
res = []
for k, (R_, G_) in pairs.items():
for lv in LEVS:
jj = (eval_sim(R_, G_, ev2, lv, "HYRO")["p_pass"]
* funded_sim(R_, G_, fu2, lv, "HYRO", 100_000.0)["p_alive"])
res.append((k, lv, jj, R_.mean() * 365 / (R_.std() * np.sqrt(365))))
bk, blv, bj, bsh = max(res, key=lambda x: x[2])
j75 = max(x[2] for x in res if x[0] == "75/25/0")
print(f" {lab:>26} {nh:>7d} {bk.rsplit('/', 1)[0]:>15} {blv:>6.3f} {bj:>7.3f} "
f"{j75:>12.3f} {bsh:>14.2f}")
print("\n Se anche sulla finestra che CONTIENE il 2022 l'ottimo scarta TP01, la sua sotto-")
print(" pesatura non e' un artefatto di finestra. Se invece li' TP01 pesa, allora la")
print(" raccomandazione del 2024+ e' una scommessa sull'assenza di crash — e su una barriera")
print(" di rovina quella scommessa la paghi una volta sola. **Questa finestra NON puo' dirlo")
print(" per XS01**, che nasce nel 2024: il buco resta aperto per lui, per costruzione.")
# ---------------------------------------------------------------- F. VRP01 / GTAA01
tick("E. banda ancora")
hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti")
print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per")
print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit")
print(" (VRP01) ne' ETF azionari USA (GTAA01, che al retail UE e' pure bloccato dal PRIIPs).")
print(" Qui si misura comunque il CONTROFATTUALE: se fossero disponibili, aiuterebbero?")
print(f"\n {'aggiunta al ' + best['w']:>34} {'leva':>6} {'J':>7} {'P(pass)':>8} "
f"{'P(vivo)':>8} {'Sharpe':>7} {'E[pay/a]':>10}")
for extra, wx in [(None, 0.0), ("VRP01", 0.10), ("VRP01", 0.20),
("GTAA01", 0.10), ("GTAA01", 0.20)]:
w2 = {k: v * (1.0 - wx) for k, v in w_best.items()}
if extra:
w2[extra] = wx
rows = [(lv, objective(w2, lv, "HYRO", ev_idx, fu_idx)) for lv in LEVS]
lv, o = max(rows, key=lambda x: x[1]["J"])
lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}"
print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} "
f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$")
print("\n Le righe VRP01 e GTAA01 escono quasi IDENTICHE: non e' un errore, e' che sotto questa")
print(" funzione obiettivo i due sleeve sono lo stesso oggetto (vol ~6.2%, drift ~7.7%, corr ~0")
print(" col crypto). La barriera vede vol, drift e correlazione; la differenza fra i due — skew")
print(" -4.6 contro -0.4 — a peso 10-20% e' diluita sotto la soglia di daily-loss.")
print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non")
print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il")
print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al")
print(" 6% verrebbe sfondata dal mark, non dal risultato.")
# ---------------------------------------------------------------- G. FTMO
tick("F. VRP/GTAA")
hr("G. LA BARRIERA CAMBIA LA RISPOSTA? — HYRO (-6% statico) vs FTMO (-10% statico)")
print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} "
f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}")
for fm in ("HYRO", "FTMO"):
g = G3 if fm == "HYRO" else grid_scan(grid, LEVS, fm, ev_idx, fu_idx)
b = g.loc[g["J"].idxmax()]
lv2 = g[g["w"] == "75/25/0"]
b2 = lv2.loc[lv2["J"].idxmax()]
print(f" {fm:>6} {b['w']:>16} {b['lev']:>6.3f} {b['J']:>7.3f} {b['p_pass']:>8.1%} "
f"{b['p_alive']:>11.1%} {b['e_payout']:>9,.0f}$ | {b2['lev']:>17.3f} {b2['J']:>7.3f}")
print("\n La barriera piu' larga di FTMO sposta l'ottimo di leva verso l'ALTO: il vincolo e' la")
print(" barriera, non il libro. E' la stessa lettura del 25/07 §4 ('sul canale funded il")
print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.")
# ---------------------------------------------------------------- H. gate ufficiale sui pesi
tick("G. FTMO")
hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) — sullo SHARPE, come previsto")
cols = {nm: off[nm] for nm in CRYPTO_PROP}
try:
res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001},
{nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP},
floor=0.0, n=200, k_seen=len(grid) * len(LEVS))
print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | "
f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}° "
f"(best-of-k {res['bestofk_pctl']:.1f}°) | gate_pass = {res['gate_pass']}")
except Exception as exc: # pragma: no cover
print(f" non girato: {exc}")
print(" NB: questo gate giudica lo SHARPE del book di ricerca, non J. Un cambio dei pesi del")
print(" LIBRO LIVE resta soggetto a lui; qui si progetta un conto SEPARATO (funded), che non")
print(" e' il libro live e non passa da questo gate.")
# ---------------------------------------------------------------- I. tavola operativa
tick("H. gate pesi")
hr("I. TAVOLA OPERATIVA — le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g")
top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3)
rows_ops = []
fu3 = boot_idx(n_hist, 1095, N_PATHS, seed=8226)
print(f" {'pesi TP/SKH/XS':>16} {'leva':>6} {'P(pass)':>8} {'P(vivo12m)':>11} "
f"{'E[payout 3a]':>13} {'EUR/g per conto':>16} {'conti per 50/g':>15}")
for _, r in top.iterrows():
w2 = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in r["w"].split("/")]))
R_, G_ = book_RG(w2)
R_ = deluck(R_)
f3 = funded_sim(R_, G_, fu3, float(r["lev"]), "HYRO", 100_000.0)
eur_day = f3["e_payout"] / EURUSD / 3.0 / 365.0
n_acct = (TARGET_EUR_DAY / eur_day) if eur_day > 0 else np.inf
rows_ops.append((r["w"], float(r["lev"]), f3["e_payout"], eur_day, n_acct))
print(f" {r['w']:>16} {r['lev']:>6.3f} {r['p_pass']:>8.1%} {r['p_alive']:>11.1%} "
f"{f3['e_payout']:>12,.0f}$ {eur_day:>15.2f} {n_acct:>15.1f}")
print("\n E[payout 3a] e' su UN conto funded da $100k, gia' netto split 80% e fisco 33%, e")
print(" MEDIA su tutti i percorsi (i morti contano zero). 'conti per 50/g' e' quindi un numero")
print(" ATTESO, non una garanzia: la distribuzione e' bimodale (vivo / bustato).")
cap_tot = sum(f["cap"] for f in pl.FIRMS)
print(f" ⚠ TETTO STRUTTURALE: cap per trader ${FIRMS['HYRO']['cap']:,.0f} (HYRO) + "
f"${FIRMS['FTMO']['cap']:,.0f} (FTMO) = ${cap_tot:,.0f} di nozionale su due firm")
print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono "
f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).")
tick("I. tavola operativa")
hr("I-bis. LA SCALA DI CONTI — macchineria del 25/07 (`r0725_prop_coupled.simulate`), "
"de-luck portato a x0.89")
print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, "
f"morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, lente ACCOPPIATA")
pc.DELUCK = DELUCK # il 25/07 girava a 0.6 (poi ri-misurato troppo severo)
cfg_best = tuple(w_best.get(nm, 0.0) for nm in CRYPTO_PROP)
cfg_live = (0.75, 0.25, 0.0)
cfg_B = (0.55, 0.20, 0.25)
pols = {
f"argmax-J {best['w']} su tutti i conti": ([cfg_best], [0] * pl.MAX_CONCURRENT),
"LIVE 75/25 su tutti i conti (baseline)": ([cfg_live], [0] * pl.MAX_CONCURRENT),
"config B 55/20/25 (scelta a mano 25/07)": ([cfg_B], [0] * pl.MAX_CONCURRENT),
}
print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} "
f"{'P(>=50/g)':>10} {'P(zero)':>8}")
for pname, (cfgs, slots) in pols.items():
for lev in sorted({round(float(best["lev"]), 3), 0.75}):
r = pc.simulate(cfgs, slots, n_paths=1500, lev=lev, seed=20260822,
lens="coupled", start=START_WIN)
print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} "
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
print("\n ⚠ QUESTO NUMERO REVISIONA UN HEADLINE DI CLAUDE.md ('EUR600 -> 50 EUR/g ~ 6% in 3")
print(" anni, P(perdere i 600) 52-65%', 25/07). Due cose sono cambiate insieme: il fattore di")
print(" de-luck (0.60 -> 0.89, ri-misurato il 26/07) e l'ALLOCAZIONE. Vanno separate, o il")
print(" merito finisce sulla leva sbagliata. Decomposizione 2x2 (stessa macchina, stesso seme):")
print(f"\n {'de-luck':>9} {'allocazione':>26} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
for fct in (0.60, 0.89):
pc.DELUCK = fct
for lab, cfg in (("LIVE 75/25 (25/07)", cfg_live), (f"argmax-J {best['w']}", cfg_best)):
r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75,
seed=20260822, lens="coupled", start=START_WIN)
print(f" {fct:>9.2f} {lab:>26} {r['med']:>10.2f} {r['p_target']:>10.1%} "
f"{r['p_zero']:>8.1%}")
print("\n ⚠ Il headline ~6% del 25/07 NON e' la riga 'LIVE su tutti i conti' (quella li' era")
print(" CONC-2SL, dichiarata 'la PEGGIORE'): era la politica MISTA, sleeve diversi su conti")
print(" diversi. Replica ESATTA di quella politica, stessa macchina, de-luck 0.60, leva 0.75:")
pc.DELUCK = 0.60
cfgs25 = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
r25 = pc.simulate(cfgs25, [1, 4, 1, 2, 1, 4], n_paths=1500, lev=0.75, seed=20260822,
lens="coupled", start=START_WIN)
print(f" MISTO (25/07) @0.75x, de-luck 0.60 : EUR/g med {r25['med']:.2f} | "
f"P(>=50/g) {r25['p_target']:.1%} | P(zero) {r25['p_zero']:.1%}")
print(f" -> il 25/07 pubblico' P(>=50/g) 5.6-6.5% e P(zero) 52% a 0.75x. "
f"{'REPLICA OK' if 0.03 <= r25['p_target'] <= 0.10 else 'NON REPLICA — leggere con cautela'}")
pc.DELUCK = DELUCK
print("\n Lettura della 2x2: il de-luck e l'allocazione contribuiscono ENTRAMBI, e vanno citati")
print(" separati. Il fattore 0.60 era una stima a occhio poi RI-MISURATA troppo severa (26/07):")
print(" usarlo ancora sarebbe sbagliato, ma il salto da 1.7% a 41.7% e' il prodotto di due")
print(" correzioni, non di una scoperta.")
hr("I-ter. LA STESSA SCALA, MA CON GLI STRESS DI E-bis/E-quater DENTRO")
print(" La tabella qui sopra gira al drift PIENO di XS01 e SKH01. E-bis mostra che il vantaggio")
print(" vive quasi tutto sul drift di XS01; E-quater che il taglio congiunto lo azzera. Il")
print(" numero operativo va quindi dato con gli stress dentro, non a parte.")
print(" (meccanica: le tuple stressate vengono INIETTATE nella cache di `pc.book_tuples`, cosi'")
print(" la simulazione della scala e' letteralmente la stessa del 25/07, con un input diverso.)")
print(f"\n {'stress':>24} {'allocazione':>16} {'leva':>6} {'EUR/g med':>10} "
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
for dmt, lab in (((), "nessuno (pieno)"),
((("SKH01", 0.50), ("XS01", 0.50)), "SKH x0.50 XS x0.50"),
((("SKH01", 0.33), ("XS01", 0.00)), "SKH x0.33 XS x0.00")):
for cfg, alab in ((cfg_best, f"argmax-J {best['w']}"), (cfg_live, "LIVE 75/25")):
wdic = dict(zip(CRYPTO_PROP, cfg))
R_, G_ = book_RG(wdic, drift_mult=dmt)
key = (float(cfg[0]), float(cfg[1]), float(cfg[2]), "shared")
saved = pc._TUP_CACHE.pop(key, None)
pc._TUP_CACHE[key] = (R_, G_, _panel_index(START_WIN))
r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75,
seed=20260822, lens="coupled", start=START_WIN)
pc._TUP_CACHE.pop(key, None)
if saved is not None:
pc._TUP_CACHE[key] = saved
print(f" {lab:>24} {alab:>16} {0.75:>6.2f} {r['med']:>10.2f} "
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
print("\n Questa e' la riga da citare: non il 41.7% pieno, ma la BANDA fra la riga piena e la")
print(" riga del taglio congiunto. Lo stress non e' un'ipotesi pessimista arbitraria: e' la")
print(" fortuna d'ancora MISURATA su SKH01 (26/07) e la finestra di scoperta di XS01 (dichiarata")
print(" in CLAUDE.md come 'taglia del guadagno ottimista').")
print("\n" + "=" * 104)
print(f" fatto in {time.time()-t0:.0f}s")
print("=" * 104)
if __name__ == "__main__":
main()
+839
View File
@@ -0,0 +1,839 @@
"""r0822_skew.py — la FORMA della superficie di volatilita' (skew), mai usata dal progetto.
Il progetto ha usato molte volte il LIVELLO della vol implicita (DVOL/ATM): VRP01 lo vende sotto
gate IV-rank, DVOLSPREAD ne fa relative-value BTC/ETH, TP01xDVOL lo prova come denominatore del
vol-target. Non ha MAI usato la FORMA: il risk reversal 25-delta (RR), il butterfly (BF) e la loro
dinamica. La catena per-strike esiste da 2026-05-01 (`data/raw/cb_chain/`), quindi la domanda e'
finalmente misurabile.
Tre domande, in ordine di valore dichiarato nel brief:
Q1 lo skew ANTICIPA lo spot? ("il posizionamento in opzioni anticipa il prezzo")
Q2 lo skew e' un GATE DI RISCHIO sopra il libro TP01?
Q3 lo skew spiega l'errore di PREZZATURA di VRP01 (f=0.73 misurato il 30/07)?
Onesta' strutturale, dichiarata PRIMA dei numeri:
* La finestra utile e' 2026-06-09 -> oggi, cioe' ~75 giorni, NON i 113 giorni di righe presenti.
Prima del 09/06 cerbero-bite raccoglieva UNA SOLA scadenza per ora (verificato: `exps=1`),
quindi non esiste ne' struttura a termine ne' maturita' costante. Righe presenti != dato
presente (regola 8 del brief), e qui il conto va fatto in ORE DI SUPERFICIE RICOSTRUIBILE.
* HOLDOUT del progetto = 2025-01-01: l'INTERO campione e' posteriore. Non esiste hold-out,
non esiste in-sample -> `study_family_honest` e `select_cell_insample` NON sono eseguibili
(l'in-sample e' vuoto). Il verdetto massimo ottenibile e' LEAD con gate pre-registrato.
* Con ~75 osservazioni giornaliere SE(Sharpe annualizzato) ~ sqrt(365/75) ~ 2.2: qualunque
Sharpe giornaliero misurato qui e' indistinguibile da zero. Percio' il peso della sessione
sta dove la POTENZA c'e' davvero: la relazione oraria skew<->prezzo (N~1.800) e la
decomposizione di prezzo di VRP01 (deterministica, non statistica).
Convenzioni di costruzione (sono SCELTE, non dettagli — vedi sezione 1):
* "quotato" = bid>0 AND ask>0 AND ask>=bid AND iv/delta non nulli. Si contano le quote, non le righe.
* smile per (asset, ora, scadenza): IV interpolata LINEARMENTE in |delta| a 0.25 e 0.50,
senza MAI estrapolare (se la catena non abbraccia il delta bersaglio, la cella si scarta).
* maturita' costante: RR/BF/ATM interpolati fra le due scadenze che abbracciano T (7g e 30g).
Il roll e' CONTINUO per costruzione: non c'e' una data di roll, quindi non c'e' il dente di
sega di tenore che un "scadenza piu' vicina a T" iniettera' nel segnale.
* ogni ora porta `ts_max` = il timestamp piu' recente fra le quote usate. I rendimenti in avanti
partono STRETTAMENTE dopo `ts_max`: senza questo l'ancora e' sbagliata di ~30 minuti e nasce
un falso "lo skew anticipa di 15 minuti" (misurato in sessione: vedi sezione 2).
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_skew.py
"""
from __future__ import annotations
import glob
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" / "alt"))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import altlib as A # noqa: E402
import cblib as CB # noqa: E402
CHAIN = ROOT / "data" / "raw" / "cb_chain"
ASSETS = ("BTC", "ETH")
COLS = ["ts", "asset", "instrument_name", "option_type", "strike", "exp",
"bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"]
# tick MISURATO sui dati (non assunto): 0.0001 sotto 0.005 di premio, 0.0005 sopra.
def tick_of(prezzo: np.ndarray) -> np.ndarray:
return np.where(np.asarray(prezzo, float) < 0.005, 0.0001, 0.0005)
def _ms(ix: pd.DatetimeIndex) -> np.ndarray:
"""Epoca esplicita in millisecondi. Lezione 01/07: `.view("int64")` (e `.astype`) su un
DatetimeIndex tz-aware non-ns sbaglia scala o solleva -> merge_asof broadcasta in silenzio."""
return pd.DatetimeIndex(ix).tz_convert("UTC").tz_localize(None).astype("datetime64[ms]").astype("int64").to_numpy()
def rule(t: str) -> None:
print("\n" + "=" * 78)
print(t)
print("=" * 78)
# =============================================================================
# 0. DATO
# =============================================================================
def load_quoted() -> tuple[pd.DataFrame, dict]:
"""Catena filtrata alle QUOTE vere. Ritorna anche la contabilita' righe-vs-quote."""
parts, n_righe, n_status = [], 0, {}
for f in sorted(glob.glob(str(CHAIN / "*.parquet"))):
d = pd.read_parquet(f, columns=COLS)
n_righe += len(d)
for k, v in d["quote_status"].value_counts(dropna=False).items():
n_status[str(k)] = n_status.get(str(k), 0) + int(v)
d["ts"] = pd.to_datetime(d["ts"], utc=True).dt.tz_convert("UTC")
d["exp"] = pd.to_datetime(d["exp"], utc=True).dt.tz_convert("UTC")
d["strike"] = d["strike"].astype(float)
parts.append(d)
d = pd.concat(parts, ignore_index=True)
d = d.drop_duplicates(subset=["ts", "instrument_name"], keep="last")
d["ts"] = pd.DatetimeIndex(d["ts"]).as_unit("ns")
d["exp"] = pd.DatetimeIndex(d["exp"]).as_unit("ns")
d["dte"] = (d["exp"] - d["ts"]).dt.total_seconds() / 86400.0
ok = (d["bid"] > 0) & (d["ask"] > 0) & (d["ask"] >= d["bid"]) & d["iv"].notna() & d["delta"].notna()
cont = dict(righe=n_righe, dedup=int(len(d)), quotate=int(ok.sum()),
frac_quotate=round(float(ok.mean()), 4), quote_status=n_status)
q = d[ok & (d["dte"] > 0.5) & (d["dte"] < 95)].copy()
q["hr"] = q["ts"].dt.floor("h")
q["ad"] = q["delta"].abs()
# una riga per (ora, strumento): se in un'ora ci sono piu' giri, vale l'ULTIMO
q = q.sort_values("ts").drop_duplicates(subset=["hr", "asset", "instrument_name"], keep="last")
return q, cont
def conta_tick(q: pd.DataFrame) -> pd.DataFrame:
"""Quanti tick vale lo spread, e quanti tick vale l'ask? (regola: prima di credere a un
rapporto estremo su un prezzo piccolo, contare i tick)."""
out = []
for a in ASSETS:
for nome, m in (("ala |d|~0.10", (q["ad"] > 0.06) & (q["ad"] < 0.14)),
("25-delta", (q["ad"] > 0.20) & (q["ad"] < 0.30)),
("ATM |d|~0.50", (q["ad"] > 0.45) & (q["ad"] < 0.55))):
g = q[(q["asset"] == a) & m]
if g.empty:
continue
tk = tick_of(g["ask"].to_numpy())
spread_tick = (g["ask"].to_numpy() - g["bid"].to_numpy()) / tk
ask_tick = g["ask"].to_numpy() / tk
out.append(dict(asset=a, banda=nome, n=len(g),
ask_tick_med=round(float(np.median(ask_tick)), 1),
ask_tick_p5=round(float(np.percentile(ask_tick, 5)), 1),
frac_ask_le2tick=round(float((ask_tick <= 2).mean()), 4),
spread_tick_med=round(float(np.median(spread_tick)), 1),
spread_su_mid_med=round(float(np.median(
(g["ask"] - g["bid"]) / g["mid"].replace(0, np.nan))), 3)))
return pd.DataFrame(out)
# =============================================================================
# 1. SUPERFICIE: smile -> RR/BF a maturita' costante
# =============================================================================
def _iv_at(ad: np.ndarray, iv: np.ndarray, tgt: float) -> float:
o = np.argsort(ad)
ad, iv = ad[o], iv[o]
if len(ad) < 2 or tgt < ad[0] or tgt > ad[-1]:
return np.nan # nessuna estrapolazione, mai
return float(np.interp(tgt, ad, iv))
def build_smile(q: pd.DataFrame) -> pd.DataFrame:
"""Per (asset, ora, scadenza): IV a |delta|=0.25 su entrambe le ali e ATM a |delta|=0.50.
Vettorizzato per bracketing (nessun ciclo per gruppo): per ogni chiave si prende la riga con
|delta| massimo SOTTO il bersaglio e quella con |delta| minimo SOPRA, poi si interpola fra
le due. Se una delle due manca, la cella si scarta -> non si estrapola MAI (una IV a 25 delta
ricavata estrapolando dall'ala e' un numero inventato, e sarebbe proprio dove il segnale
sembrerebbe piu' forte)."""
KEY = ["asset", "hr", "exp", "option_type"]
def bracket(d: pd.DataFrame, tgt: float) -> pd.DataFrame:
d = d[["asset", "hr", "exp", "option_type", "ad", "iv"]]
lo = d[d["ad"] <= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).tail(1)
hi = d[d["ad"] >= tgt].sort_values("ad").groupby(KEY, sort=False, observed=True).head(1)
m = lo.merge(hi, on=KEY, suffixes=("_lo", "_hi"))
span = (m["ad_hi"] - m["ad_lo"]).to_numpy()
w = np.where(span > 1e-12, (tgt - m["ad_lo"].to_numpy()) / np.where(span > 1e-12, span, 1.0), 0.0)
m["iv_t"] = m["iv_lo"].to_numpy() * (1 - w) + m["iv_hi"].to_numpy() * w
return m[KEY + ["iv_t"]]
# bastano le scadenze che possono ABBRACCIARE 7 o 30 giorni
d = q[q["dte"] <= 70.0]
b25 = bracket(d, 0.25).pivot_table(index=["asset", "hr", "exp"], columns="option_type",
values="iv_t", observed=True)
b50 = bracket(d, 0.50).pivot_table(index=["asset", "hr", "exp"], columns="option_type",
values="iv_t", observed=True)
R = pd.DataFrame(index=b25.index)
R["ivc25"] = b25.get("C")
R["ivp25"] = b25.get("P")
R = R.dropna(subset=["ivc25", "ivp25"]) # servono ENTRAMBE le ali, sempre
R["ivatm"] = b50.reindex(R.index)[[c for c in ("C", "P") if c in b50.columns]].mean(axis=1)
agg = d.groupby(["asset", "hr", "exp"], sort=False, observed=True).agg(
dte=("dte", "first"), ts_max=("ts", "max"), n_gambe=("iv", "size"))
R = R.join(agg, how="left").reset_index()
R["rr"] = R["ivc25"] - R["ivp25"]
R["bf"] = 0.5 * (R["ivc25"] + R["ivp25"]) - R["ivatm"]
return R
def const_maturity(R: pd.DataFrame, asset: str, T: float) -> pd.DataFrame:
"""RR/BF/ATM a maturita' costante T giorni. Interpolazione lineare in DTE fra le due scadenze
che ABBRACCIANO T (mai estrapolazione) -> il roll e' continuo, non c'e' data di roll."""
g = R[(R["asset"] == asset)].dropna(subset=["rr", "ivatm"])
out = {}
for hr, h in g.groupby("hr"):
h = h.sort_values("dte")
dt = h["dte"].to_numpy()
if len(h) < 2 or T < dt[0] or T > dt[-1]:
continue
out[hr] = dict(rr=float(np.interp(T, dt, h["rr"].to_numpy())),
bf=float(np.interp(T, dt, h["bf"].to_numpy())),
atm=float(np.interp(T, dt, h["ivatm"].to_numpy())),
ts_max=h["ts_max"].max())
S = pd.DataFrame.from_dict(out, orient="index").sort_index()
S.index.name = "hr"
if len(S):
S["rr_n"] = S["rr"] / S["atm"] # skew normalizzato dal livello (toglie il DVOL)
return S
# =============================================================================
# 2. Q1 — lo skew ANTICIPA lo spot?
# =============================================================================
def px5m(asset: str, t0: pd.Timestamp) -> pd.Series:
p = pd.read_parquet(ROOT / "data" / "raw" / f"{asset.lower()}_5m.parquet",
columns=["timestamp", "close"])
s = pd.Series(p["close"].to_numpy(float),
index=pd.DatetimeIndex(pd.to_datetime(p["timestamp"], unit="ms", utc=True)))
return s.sort_index().loc[t0 - pd.Timedelta("3h"):]
def leadlag(S: pd.DataFrame, asset: str) -> pd.DataFrame:
"""Correlazione fra la variazione oraria dello skew e i rendimenti PRIMA e DOPO l'istante
esatto in cui le quote sono state osservate (`ts_max`). L'ancora conta: con un'ancora
assunta a :30 invece del `ts_max` vero compare un falso lead a +15m."""
S = S.sort_values("ts_max")
px = px5m(asset, S.index.min())
anc = pd.Series(px.reindex(pd.DatetimeIndex(S["ts_max"]), method="ffill").to_numpy(),
index=S.index)
drr = S["rr"].diff()
lvl = S["rr"]
rows = []
for mins in (-60, -30, -15, -5, 5, 15, 30, 60, 120, 240, 1440, 4320):
tgt = pd.DatetimeIndex(S["ts_max"]) + pd.Timedelta(minutes=int(mins))
p2 = pd.Series(px.reindex(tgt, method="ffill").to_numpy(), index=S.index)
r = np.log(p2 / anc) if mins > 0 else np.log(anc / p2) # sempre "rendimento nel verso del tempo"
j = pd.concat({"d": drr, "l": lvl, "r": r}, axis=1).dropna()
n = len(j)
rows.append(dict(asset=asset, minuti=mins, n=n,
corr_dRR=round(float(j["d"].corr(j["r"])), 3),
corr_RR=round(float(j["l"].corr(j["r"])), 3),
se=round(1 / np.sqrt(max(n, 2)), 3),
t_dRR=round(float(j["d"].corr(j["r"]) * np.sqrt(n)), 2)))
return pd.DataFrame(rows)
def leadlag_ancora_sbagliata(S: pd.DataFrame, asset: str) -> pd.DataFrame:
"""Controllo POSITIVO del punto precedente: la stessa misura con l'ancora ASSUNTA a :30
dell'ora (invece del `ts_max` osservato) deve mostrare il falso lead. Se non lo mostra,
la correzione non stava correggendo niente."""
px = px5m(asset, S.index.min())
fake = pd.DatetimeIndex(S.index) + pd.Timedelta("30min")
anc = pd.Series(px.reindex(fake, method="ffill").to_numpy(), index=S.index)
drr = S["rr"].diff()
rows = []
for mins in (5, 15, 30, 60):
p2 = pd.Series(px.reindex(fake + pd.Timedelta(minutes=mins), method="ffill").to_numpy(),
index=S.index)
j = pd.concat({"d": drr, "r": np.log(p2 / anc)}, axis=1).dropna()
rows.append(dict(asset=asset, minuti=mins, corr_dRR_ancora_finta=round(float(j["d"].corr(j["r"])), 3)))
return pd.DataFrame(rows)
def daily_signal(S: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
"""Allinea la superficie alla griglia dei prezzi in modo CAUSALE.
Le barre del progetto sono open-labeled: la barra 1d etichettata D chiude alle 23:00 di D.
Quindi al momento della decisione della barra D e' disponibile l'ultima ora <= D 23:00, e
`eval_weights` la tiene durante D+1. Con merge_asof su epoca esplicita in millisecondi
(lezione 01/07: `.view("int64")` su tz-aware non-ns sbaglia scala e broadcasta)."""
bpd = A.bars_per_day(df)
chiusura = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
chiusura = chiusura + (pd.Timedelta("24h") - pd.Timedelta("1h") if bpd == 1 else pd.Timedelta(0))
left = pd.DataFrame({"t_ms": _ms(chiusura)})
right = pd.DataFrame({"t_ms": _ms(pd.DatetimeIndex(S["ts_max"])),
"rr": S["rr"].to_numpy(), "rr_n": S["rr_n"].to_numpy(),
"bf": S["bf"].to_numpy(), "atm": S["atm"].to_numpy()}).sort_values("t_ms")
m = pd.merge_asof(left.sort_values("t_ms"), right, on="t_ms", direction="backward",
tolerance=int(pd.Timedelta("6h").total_seconds() * 1000))
return m[["rr", "rr_n", "bf", "atm"]]
def make_target(S: pd.DataFrame, feat: str, win: int, segno: int):
"""target_fn(df, asset) -> posizione continua vol-targeted. Causale per costruzione:
ogni feature usa solo z-score rolling su dati <= barra corrente, e `eval_weights` sposta."""
def fn(df, asset=None):
F = daily_signal(S, df)
x = F["rr_n"] if feat.endswith("_n") else F["rr"]
if feat.startswith("d"):
x = x.diff()
z = (x - x.rolling(win, min_periods=max(5, win // 2)).mean()) / \
x.rolling(win, min_periods=max(5, win // 2)).std()
d = np.tanh(np.nan_to_num(z.to_numpy(), nan=0.0)) * segno
return A.vol_target(d, df, target_vol=0.20, leverage_cap=2.0)
return fn
def sharpe_boot(s: pd.Series, blocco: int = 10, n: int = 500, seed: int = 20260822) -> tuple:
"""IC95% dello Sharpe con block bootstrap circolare (il segnale e' lentissimo: i giorni
NON sono indipendenti, e una SE analitica mentirebbe)."""
r = np.asarray(s.dropna().to_numpy(), float)
T = len(r)
if T < 20:
return (np.nan, np.nan)
rng = np.random.default_rng(seed)
nb = int(np.ceil(T / blocco))
out = []
for _ in range(n):
st = rng.integers(0, T, nb)
idx = (st[:, None] + np.arange(blocco)[None, :]).ravel()[:T] % T
x = r[idx]
out.append(np.mean(x) / np.std(x) * np.sqrt(365.25) if np.std(x) > 0 else 0.0)
return (round(float(np.percentile(out, 2.5)), 2), round(float(np.percentile(out, 97.5)), 2))
def causality_finestra(S: pd.DataFrame, fn, asset: str) -> dict:
"""`A.causality_ok` taglia il prefisso all'80%/92% della storia dei PREZZI (2024/2025):
fuori dalla finestra dello skew, dove il segnale e' zero -> confronterebbe zeri con zeri,
POTENZA NULLA (stessa trappola del self-check di SKH01: si campiona sugli EVENTI).
Qui i tagli cadono DENTRO la finestra."""
df = A.get(asset, "1d")
full = np.nan_to_num(np.asarray(fn(df, asset), float))
n = len(df)
dts = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
dentro = np.where(dts >= S.index.min())[0]
peggio, controlli = 0.0, 0
for frac in (0.4, 0.7, 0.9):
cut = int(dentro[0] + frac * (n - dentro[0]))
if cut <= dentro[0] + 5 or cut >= n:
continue
sub = df.iloc[:cut].reset_index(drop=True)
s = np.nan_to_num(np.asarray(fn(sub, asset), float))
coda = min(20, cut - dentro[0])
peggio = max(peggio, float(np.max(np.abs(s[cut - coda:cut] - full[cut - coda:cut]))))
controlli += 1
return dict(ok=bool(peggio <= 1e-6 and controlli > 0), max_diff=round(peggio, 10),
controlli=controlli, non_zero=int((full[dentro] != 0).sum()))
# =============================================================================
# 3. Q2 — lo skew come GATE DI RISCHIO sopra TP01
# =============================================================================
def dd_of(r: np.ndarray) -> float:
eq = np.cumprod(1.0 + np.nan_to_num(r))
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def sh_of(r: np.ndarray) -> float:
r = np.asarray(r, float)
return float(np.mean(r) / np.std(r) * np.sqrt(365.25)) if np.std(r) > 0 else 0.0
def null_de_levering(base: np.ndarray, overlay: np.ndarray) -> dict:
"""Il primo test di OGNI claim su meno drawdown: esiste k<=1 che, applicato al baseline,
da' lo STESSO DD dell'overlay ma con Sharpe MIGLIORE? Se si', l'overlay e' solo meno leva."""
dd_t = dd_of(overlay)
lo, hi = 0.0, 1.0
for _ in range(60):
k = 0.5 * (lo + hi)
if dd_of(k * base) > dd_t:
hi = k
else:
lo = k
k = 0.5 * (lo + hi)
return dict(k=round(k, 3), dd_overlay=round(dd_t, 4), dd_base_k=round(dd_of(k * base), 4),
sh_overlay=round(sh_of(overlay), 3), sh_base_k=round(sh_of(k * base), 3),
superato=bool(sh_of(overlay) > sh_of(k * base)))
# =============================================================================
# 4. Q3 — lo skew spiega l'errore di prezzo di VRP01?
# =============================================================================
def vrp_decomposizione(q: pd.DataFrame, cm7: dict) -> pd.DataFrame:
"""Per ogni ingresso settimanale di VRP01 (stessa macchina di `cblib`, cosi' i numeri sono
confrontabili con lo 0.718 pubblicato il 30/07), scompone il f in fattori MOLTIPLICATIVI:
f_tot = f_term x f_skew x f_markfit x f_spread
f_term = credito prezzato ad ATM 7g piatta / credito prezzato a DVOL30 piatta
(quanto del difetto e' STRUTTURA A TERMINE: il sleeve prezza un'opzione a 7
giorni con l'indice a 30 giorni)
f_skew = credito prezzato a IV PER GAMBA / credito ad ATM 7g piatta
(quanto e' SKEW: l'ala che si compra non vale la vol ATM)
f_markfit = credito ai MID reali / credito a IV per gamba (controllo: deve stare a ~1,
altrimenti la IV di Deribit non riprezza i propri mid e tutta la catena di
ragionamento poggia su un modello invece che su un prezzo)
f_spread = credito al fill CONSERVATIVO (vendi al bid, compri all'ask) / credito ai mid
"""
righe = []
for a in ASSETS:
S7 = cm7[a]
E = CB.weekly_entries(a, q)
for _, e in E.iterrows():
prima = S7.index[S7.index <= e["ts"]]
if len(prima) == 0 or (e["ts"] - prima[-1]) > pd.Timedelta("6h"):
continue
s = S7.loc[prima[-1]]
T = e["dte"] / 365.25
dvol = e["dvol_pct"] / 100.0
atm7 = s["atm"] / 100.0
c_mod = CB.bs_put(e["spot"], e["k_short"], T, dvol) - CB.bs_put(e["spot"], e["k_long"], T, dvol)
c_atm = CB.bs_put(e["spot"], e["k_short"], T, atm7) - CB.bs_put(e["spot"], e["k_long"], T, atm7)
c_sml = (CB.bs_put(e["spot"], e["k_short"], T, e["iv_short"] / 100.0)
- CB.bs_put(e["spot"], e["k_long"], T, e["iv_long"] / 100.0))
c_mid = e["cred_mid"]
c_real = e["cred_real"]
righe.append(dict(
asset=a, ts=e["ts"], dte=round(e["dte"], 2), ivrank=round(e["ivrank"], 3),
dvol=round(e["dvol_pct"], 2), atm7=round(s["atm"], 2),
rr7=round(s["rr"], 2), bf7=round(s["bf"], 2),
iv_short=round(e["iv_short"], 2), iv_long=round(e["iv_long"], 2),
d_short=round(e["d_short"], 3), d_long=round(e["d_long"], 3),
f_term=c_atm / c_mod if c_mod > 0 else np.nan,
f_skew=c_sml / c_atm if c_atm > 0 else np.nan,
f_markfit=c_mid / c_sml if c_sml > 0 else np.nan,
f_spread=c_real / c_mid if c_mid > 0 else np.nan,
f_tot_mid=c_mid / c_mod if c_mod > 0 else np.nan,
f_tot=c_real / c_mod if c_mod > 0 else np.nan,
quota_ala=CB.bs_put(e["spot"], e["k_long"], T, dvol) /
CB.bs_put(e["spot"], e["k_short"], T, dvol),
))
return pd.DataFrame(righe)
MARKFIT_OK = (0.85, 1.15) # banda del CONTROLLO, dichiarata prima di guardare i f
def qualita(D: pd.DataFrame) -> pd.Series:
"""Filtro dichiarato: si tiene un'osservazione solo se il CONTROLLO passa, cioe' se la IV di
Deribit riprezza i suoi stessi mid entro +-15% sul credito, e se il credito conservativo e'
positivo. NON e' un filtro sull'esito (il f), e' un filtro sulla variabile di controllo:
dove il controllo fallisce non si sta misurando lo skew, si sta misurando una quota rotta."""
return (D["f_markfit"].between(*MARKFIT_OK)) & (D["f_spread"] > 0) & D["f_term"].gt(0)
def attribuzione_log(D: pd.DataFrame) -> pd.DataFrame:
"""Attribuzione ESATTA e additiva: log(f_tot) = log(f_term)+log(f_skew)+log(f_markfit)+
log(f_spread), per costruzione (la catena dei rapporti telescopia). Le MEDIANE dei fattori
non si moltiplicano fra loro; i log si sommano. Da qui la QUOTA del difetto per causa."""
g = D[qualita(D)]
parti = ["f_term", "f_skew", "f_markfit", "f_spread"]
L = np.log(g[parti])
tot = float(np.log(g["f_tot"]).mean())
rows = []
for c in parti:
m = float(L[c].mean())
rows.append(dict(fattore=c, media_log=round(m, 4),
fattore_geometrico=round(float(np.exp(m)), 3),
quota_del_difetto=round(m / tot, 3) if tot != 0 else np.nan))
rows.append(dict(fattore="TOTALE", media_log=round(tot, 4),
fattore_geometrico=round(float(np.exp(tot)), 3), quota_del_difetto=1.0))
return pd.DataFrame(rows)
def panel_f(q: pd.DataFrame, cm7: dict) -> pd.DataFrame:
"""La stessa decomposizione, ma a OGNI ora in cui lo spread e' costruibile (non solo ai 22
ingressi settimanali di VRP01). Serve alla RISOLUZIONE: 22 punti non separano due cause da
~10% l'una. ⚠ Le ore dentro la stessa scadenza NON sono indipendenti: il campione efficace
resta il numero di SCADENZE, che si riporta accanto."""
out = []
for a in ASSETS:
S7 = cm7[a]
S, V = CB.spot_series(a), CB.dvol_series(a)
p = q[(q["asset"] == a) & (q["option_type"] == "P")
& (q["dte"] >= 4.0) & (q["dte"] <= 10.0)]
for (hr, exp), g in p.groupby(["hr", "exp"], sort=False):
if hr not in S7.index:
continue
legs = CB.pick_legs(g)
if legs is None:
continue
ts = g["ts"].max()
dte = float((exp - ts).total_seconds() / 86400.0)
if not (4.0 <= dte <= 10.0):
continue
spot = float(S.asof(ts)); dvol = float(V.asof(ts)) / 100.0
atm7 = float(S7.loc[hr, "atm"]) / 100.0
T = dte / 365.25
c_mod = CB.bs_put(spot, legs["k_short"], T, dvol) - CB.bs_put(spot, legs["k_long"], T, dvol)
c_atm = CB.bs_put(spot, legs["k_short"], T, atm7) - CB.bs_put(spot, legs["k_long"], T, atm7)
c_sml = (CB.bs_put(spot, legs["k_short"], T, legs["iv_short"] / 100.0)
- CB.bs_put(spot, legs["k_long"], T, legs["iv_long"] / 100.0))
c_mid = (legs["mid_short"] - legs["mid_long"]) * spot
c_real = (legs["bid_short"] - legs["ask_long"]) * spot
if not (c_mod > 0 and c_atm > 0 and c_sml > 0):
continue
hist = V[V.index < ts]
ivr = float((hist < dvol * 100).mean()) if len(hist) else np.nan
out.append(dict(asset=a, hr=hr, exp=exp, dte=dte, rr7=float(S7.loc[hr, "rr"]),
atm7=float(S7.loc[hr, "atm"]), dvol=dvol * 100, ivrank=ivr,
f_term=c_atm / c_mod, f_skew=c_sml / c_atm,
f_markfit=c_mid / c_sml, f_spread=c_real / c_mid,
f_tot=c_real / c_mod, f_tot_mid=c_mid / c_mod))
return pd.DataFrame(out)
# =============================================================================
def main() -> None:
t_start = time.time()
rule("r0822_skew — SKEW (risk reversal 25-delta): la dimensione mai usata della superficie")
# ---------------------------------------------------------------- 0. DATO
rule("0. IL DATO — quote, non righe")
q, cont = load_quoted()
print(f" righe totali nella catena : {cont['righe']:,}")
print(f" dopo dedup (ts, strumento) : {cont['dedup']:,}")
print(f" QUOTATE (bid>0, ask>0, iv/delta ok) : {cont['quotate']:,} = {cont['frac_quotate']:.1%}")
print(f" quote_status : {cont['quote_status']}")
print(f" usate (0.5<DTE<95, ultimo giro/ora) : {len(q):,}")
print(f" finestra righe : {q['ts'].min()} -> {q['ts'].max()}")
print("\n TICK (regola: prima di credere a un rapporto estremo su un prezzo piccolo, contarli)")
print(conta_tick(q).to_string(index=False))
# ---------------------------------------------------------- 1. SUPERFICIE
rule("1. SUPERFICIE — smile per delta, poi maturita' costante")
t0 = time.time()
R = build_smile(q)
print(f" celle (asset, ora, scadenza) con smile ricostruibile: {len(R):,} [{time.time()-t0:.0f}s]")
perday = R.groupby(["asset", pd.DatetimeIndex(R["hr"]).date]).agg(exps=("exp", "nunique"))
print(f" giorni con >=2 scadenze (maturita' costante possibile): "
f"{int((perday['exps'] >= 2).sum())} / {len(perday)} coppie asset-giorno")
cm7, cm30 = {}, {}
for a in ASSETS:
cm7[a] = const_maturity(R, a, 7.0)
cm30[a] = const_maturity(R, a, 30.0)
s = cm30[a]
print(f" {a}: CM30 ore={len(s):5d} giorni={len(np.unique(pd.DatetimeIndex(s.index).date)):3d}"
f" {s.index.min()} -> {s.index.max()}")
print(f" RR30 media {s['rr'].mean():+.2f} pp sd {s['rr'].std():.2f} "
f"[{s['rr'].min():+.2f}, {s['rr'].max():+.2f}] ATM media {s['atm'].mean():.1f}% "
f"BF30 media {s['bf'].mean():+.2f}")
print(f" CM7 ore={len(cm7[a]):5d} RR7 media {cm7[a]['rr'].mean():+.2f} sd {cm7[a]['rr'].std():.2f}")
print("\n ⚠ La finestra utile parte dal 2026-06-09 e NON dal 2026-05-01: prima di quella data")
print(" cerbero-bite raccoglieva UNA sola scadenza per ora -> nessuna maturita' costante.")
print(" righe presenti != superficie presente. Le ore perse sono ~38 giorni su 113.")
# ------------------------------------------------------------------ 2. Q1
rule("2. Q1 — lo skew ANTICIPA lo spot? (potenza vera: N orario ~1.800)")
LL = pd.concat([leadlag(cm30[a], a) for a in ASSETS], ignore_index=True)
for a in ASSETS:
print(f"\n {a} — corr fra dRR (variazione oraria dello skew) e il rendimento nella")
print(" finestra indicata, ancorata al TIMESTAMP VERO delle quote (ts_max):")
print(LL[LL["asset"] == a].to_string(index=False))
fw = LL[LL["minuti"] > 0]
print(f"\n LAG IN AVANTI: {len(fw)} test ({fw['minuti'].nunique()} orizzonti x {len(ASSETS)} asset).")
print(f" |t| massimo osservato = {fw['t_dRR'].abs().max():.2f} su {len(fw)} test;")
print(f" |t| atteso come MASSIMO di {len(fw)} test indipendenti sotto il nullo ~ "
f"{float(np.abs(np.random.default_rng(0).standard_normal((4000, len(fw)))).max(axis=1).mean()):.2f}.")
print(" -> il massimo in avanti e' quello che il rumore produce da solo. LAG INDIETRO:")
print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su "
f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.")
print("\n Robustezza — il COLLETTORE cambia a meta' campione (cerbero-bite: ~10 giri/ora,")
print(" fino al 2026-07-30; raccolta nostra: 1 giro/ora al minuto :25) e il campione contiene")
print(" UN evento enorme (BTC +26% nella settimana al 21/08). Le due cose vanno separate:")
TAGLIO = pd.Timestamp("2026-07-30 20:00", tz="UTC")
RALLY = pd.to_datetime(["2026-08-19", "2026-08-20", "2026-08-21", "2026-08-22"]).tz_localize("UTC")
for a in ASSETS:
S = cm30[a]
gg_rally = pd.DatetimeIndex(S.index).normalize().isin(RALLY)
for nome, m in (("bite ", S.index < TAGLIO),
("nostra ", S.index >= TAGLIO),
("nostra SENZA rally", (S.index >= TAGLIO) & ~gg_rally)):
sub = S[m].sort_values("ts_max")
if len(sub) < 100:
continue
px = px5m(a, sub.index.min())
anc = pd.Series(px.reindex(pd.DatetimeIndex(sub["ts_max"]), method="ffill").to_numpy(),
index=sub.index)
d = sub["rr"].diff()
av = []
for mins in (5, 15, 60):
p2 = pd.Series(px.reindex(pd.DatetimeIndex(sub["ts_max"]) + pd.Timedelta(minutes=mins),
method="ffill").to_numpy(), index=sub.index)
j = pd.concat({"d": d, "r": np.log(p2 / anc)}, axis=1).dropna()
av.append((round(float(j["d"].corr(j["r"])), 3),
round(float(j["d"].corr(j["r"], method="spearman")), 3)))
print(f" {a} {nome} n={len(sub):5d} avanti +5/+15/+60m (pearson,spearman): {av}")
print(" -> l'unica cella con un lead in avanti apparente (BTC, raccolta nostra, +5/+15m)")
print(" PERDE i due terzi togliendo 4 giorni su 24: era il rally, non un meccanismo.")
gio = pd.Series(1, index=pd.DatetimeIndex(cm30[ASSETS[0]].index)).groupby(
pd.DatetimeIndex(cm30[ASSETS[0]].index).date).sum()
print(f" copertura: giorni con meno ore di superficie (BTC) "
f"{ {str(k): int(v) for k, v in gio.nsmallest(4).items()} } su 24 attese "
f"(il guasto quote-vuote del 29-30/07 e' dentro il campione e NON crea un buco: le")
print(" quote assenti erano gia' escluse dal filtro 'quotato').")
print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:")
FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True)
print(FF.to_string(index=False))
# strategia giornaliera
rule("2b. Q1 — lo skew come SEGNALE DIREZIONALE giornaliero (griglia dichiarata)")
feats = ["rr", "rr_n", "drr"]
wins = [10, 20, 40]
segni = [+1, -1]
celle, serie = [], {}
for f in feats:
for w in wins:
for sg in segni:
nome = f"{f}|w{w}|sg{sg:+d}"
per_asset = {}
for a in ASSETS:
df = A.get(a, "1d")
Sx = cm30[a]
m = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) >= (
Sx.index.min().floor("D"))
sub = df[m].reset_index(drop=True)
tgt = make_target(Sx, f, w, sg)(sub, a)
ev = A.eval_weights(sub, tgt)
per_asset[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(per_asset, axis=1, join="inner").fillna(0.0)
d = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]
d.index = pd.DatetimeIndex(d.index)
serie[nome] = d
celle.append(dict(cella=nome, sharpe=round(sh_of(d.to_numpy()), 3),
maxdd=round(dd_of(d.to_numpy()), 4),
cagr=round(float((1 + d).prod() ** (365.25 / len(d)) - 1), 4)))
C = pd.DataFrame(celle).sort_values("sharpe", ascending=False)
print(f" griglia: {len(feats)} feature x {len(wins)} finestre-z x {len(segni)} segni = {len(C)} celle")
print(C.head(6).to_string(index=False))
print(" ...")
print(C.tail(3).to_string(index=False))
best = C.iloc[0]["cella"]
bs = serie[best]
lo, hi = sharpe_boot(bs)
print(f"\n MIGLIORE della griglia: {best} Sharpe {C.iloc[0]['sharpe']:+.3f}")
print(f" IC95% block-bootstrap (blocchi 10g, 500 estrazioni): [{lo:+.2f}, {hi:+.2f}] n={len(bs)} giorni")
print(f" ⚠ ampiezza dell'IC = {hi-lo:.2f} di Sharpe: con {len(bs)} giorni la misura non separa")
print(" un edge da zero. Nessuna cella di questa griglia e' distinguibile dal rumore.")
dsr, sr0 = A.deflated_sharpe(C.iloc[0]["sharpe"], C["sharpe"].tolist(), bs)
print(f" deflated Sharpe della cella migliore: DSR={dsr:.3f} (soglia 0.95), "
f"max atteso sotto il nullo {sr0:+.2f}")
print(" NB: e' un deflated-Sharpe BEST-OF-GRID, non in-sample-selected: `select_cell_insample`")
print(" NON e' eseguibile qui (HOLDOUT=2025-01-01, l'in-sample e' VUOTO).")
print("\n marginal_vs_tp01 sulla cella migliore:")
try:
rep = A.marginal_vs_tp01(bs)
for k in ("marginal_verdict", "corr_full", "n_days", "has_insample_edge", "is_hedge",
"beats_noise_null", "robust_oos"):
if k in rep:
print(f" {k:20s} = {rep[k]}")
except Exception as ex:
print(f" non girato: {ex}")
imp = A.implausible_sharpe(bs)
print(f"\n implausible_sharpe sulla cella migliore: implausible={imp['implausible']} "
f"reasons={imp.get('reasons')}")
print(f" (Sharpe {imp.get('sharpe')}, maxDD {imp.get('maxdd')}, "
f"perdite sulle barre ATTIVE {imp.get('loss_frac')}, attive {imp.get('active_frac')})")
print("\n causalita' (tagli DENTRO la finestra dello skew, non all'80% della storia prezzi):")
for a in ASSETS:
f_, w_, sg_ = best.split("|")
fn = make_target(cm30[a], f_, int(w_[1:]), int(sg_.replace("sg", "")))
print(f" {a}: {causality_finestra(cm30[a], fn, a)}")
# ------------------------------------------------------------------ 3. Q2
rule("3. Q2 — lo skew come GATE DI RISCHIO sopra TP01")
B = A.tp01_baseline_daily()
inizio = min(cm30[a].index.min() for a in ASSETS).floor("D")
Bw = B[B.index >= inizio]
z_all = {}
for a in ASSETS:
df = A.get(a, "1d")
m = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) >= inizio
sub = df[m].reset_index(drop=True)
F = daily_signal(cm30[a], sub)
x = F["rr_n"]
z = (x - x.rolling(20, min_periods=10).mean()) / x.rolling(20, min_periods=10).std()
z_all[a] = pd.Series(z.to_numpy(), index=pd.DatetimeIndex(pd.to_datetime(sub["datetime"], utc=True)))
Z = pd.concat(z_all, axis=1).mean(axis=1)
Z = Z.reindex(Bw.index).ffill()
att = int((Bw != 0).sum())
print(f" baseline TP01 sulla finestra dello skew: n={len(Bw)} giorni, "
f"Sharpe {sh_of(Bw.to_numpy()):+.2f}, maxDD {dd_of(Bw.to_numpy()):.2%}")
print(f" ⚠ barre ATTIVE del baseline: {att}/{len(Bw)} ({att/len(Bw):.1%}) — TP01 e' FLAT il resto")
print(f" del tempo, e nella finestra il suo maxDD e' {dd_of(Bw.to_numpy()):.2%}: un gate di")
print(" de-risk non ha NIENTE da proteggere qui. Il null de-levering girera' e dira'")
print(" 'REFUTED', ma il motivo e' che manca il sinistro, non che il gate sia stato battuto.")
for thr in (-0.5, -1.0, -1.5):
gate = (Z.shift(1) > thr).astype(float) # de-risk quando lo skew si irripidisce (z basso)
ov = (Bw * gate).to_numpy()
n_fire = int((gate == 0).sum())
nul = null_de_levering(Bw.to_numpy(), ov)
gg = Bw[gate == 0]
print(f" soglia z<{thr:+.1f}: giorni de-riskati {n_fire:3d}/{len(Bw)} "
f"({n_fire/len(Bw):.1%}) Sharpe {sh_of(ov):+.2f} (base {sh_of(Bw.to_numpy()):+.2f}) "
f"maxDD {dd_of(ov):.2%}")
print(f" null de-levering: k={nul['k']} -> stesso DD {nul['dd_base_k']:.2%} con "
f"Sharpe {nul['sh_base_k']:+.2f} vs overlay {nul['sh_overlay']:+.2f} "
f"-> {'SUPERATO' if nul['superato'] else 'REFUTED'}")
if n_fire:
gia_flat = int((gg == 0).sum())
print(f" nei giorni de-riskati TP01 faceva in media {gg.mean()*100:+.3f}%/g "
f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g); di quei {n_fire} giorni TP01 era "
f"GIA' FLAT in {gia_flat} ({gia_flat/n_fire:.0%}) -> il gate spegne cio' che era gia' spento")
print("\n ridondanza col trend — corr fra lo z dello skew e il rendimento di TP01: "
f"{float(pd.concat({'z': Z, 'b': Bw}, axis=1).dropna().corr().iloc[0,1]):+.3f}")
print(" POTENZA: con 75 giorni un gate che scatta ~10 volte non e' misurabile. Il numero da")
print(" guardare non e' lo Sharpe dell'overlay ma il conto dei giorni in cui scatta.")
# ------------------------------------------------------------------ 4. Q3
rule("4. Q3 — lo skew SPIEGA l'errore di prezzo di VRP01 (f=0.73 pubblicato il 30/07)")
D = vrp_decomposizione(q, cm7)
print(f" ingressi settimanali ricostruiti: {len(D)} ({D.groupby('asset').size().to_dict()})")
print("\n REPLICA del numero pubblicato (deve tornare ~0.72 sul canonico 7g d-0.28/-0.10):")
print(f" f_tot (fill conservativo) mediana = {D['f_tot'].median():.3f} "
f"media = {D['f_tot'].mean():.3f}")
print(f" f_tot ai MID mediana = {D['f_tot_mid'].median():.3f}")
print("\n IV per gamba contro i due riferimenti (pp di vol):")
print(f" IV(corta) - DVOL30 : {(D['iv_short']-D['dvol']).median():+.2f} pp "
f"IV(lunga) - DVOL30 : {(D['iv_long']-D['dvol']).median():+.2f} pp [pubblicato: +0.8 / +7.5]")
print(f" IV(corta) - ATM7 : {(D['iv_short']-D['atm7']).median():+.2f} pp "
f"IV(lunga) - ATM7 : {(D['iv_long']-D['atm7']).median():+.2f} pp")
print(f" ATM7 - DVOL30 : {(D['atm7']-D['dvol']).median():+.2f} pp <- pura struttura a termine")
print("\n DECOMPOSIZIONE MOLTIPLICATIVA del f (mediane; f_tot = term x skew x markfit x spread)")
for a in ASSETS + ("TUTTI",):
g = D if a == "TUTTI" else D[D["asset"] == a]
if g.empty:
continue
print(f" {a:6s} n={len(g):2d} | f_term {g['f_term'].median():.3f} | "
f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | "
f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}")
print("\n controllo obbligatorio: f_markfit deve stare a ~1 (la IV di Deribit deve riprezzare")
print(f" i suoi stessi mid). Mediana {D['f_markfit'].median():.3f}, "
f"banda [{D['f_markfit'].quantile(.1):.3f}, {D['f_markfit'].quantile(.9):.3f}].")
print(" Se sta a 1, RR e BF di questa sessione sono un fatto di PREZZO, non un artefatto")
print(" del fit di Deribit -> vale anche come controllo di confound per Q1/Q2.")
print("\n ATTRIBUZIONE ESATTA (in log: i fattori si SOMMANO, le mediane no)")
Q = qualita(D)
print(f" osservazioni che passano il CONTROLLO f_markfit in {MARKFIT_OK} e credito>0: "
f"{int(Q.sum())}/{len(D)}")
scartate = D[~Q][["asset", "ts", "f_markfit", "f_spread"]].copy()
if len(scartate):
scartate["ts"] = pd.DatetimeIndex(scartate["ts"]).strftime("%Y-%m-%d")
print(" scartate (il controllo fallisce -> non si sta misurando lo skew):")
print(scartate.round(3).to_string(index=False))
print(attribuzione_log(D).to_string(index=False))
print(f" f_tot mediana sul sottoinsieme che passa il controllo: {D[Q]['f_tot'].median():.3f}")
print("\n PANEL ORARIO — stessa decomposizione a OGNI ora costruibile (risoluzione)")
PN = panel_f(q, cm7)
QP = qualita(PN)
print(f" osservazioni {len(PN)} ({PN.groupby('asset').size().to_dict()}), di cui passano il "
f"controllo {int(QP.sum())}; scadenze distinte (campione EFFICACE) "
f"{PN.groupby('asset')['exp'].nunique().to_dict()}")
G = PN[QP]
for a in ASSETS + ("TUTTI",):
g = G if a == "TUTTI" else G[G["asset"] == a]
if g.empty:
continue
print(f" {a:6s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | "
f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | "
f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}")
print(" attribuzione log sul panel:")
print(attribuzione_log(PN).to_string(index=False))
print("\n Il f dipende dal REGIME? (la domanda che decide se 0.73 e' conservativo o ottimista)")
D2 = D[qualita(D)].dropna(subset=["f_tot_mid", "rr7"])
for var in ("rr7", "atm7", "ivrank", "bf7"):
c = float(D2["f_tot_mid"].corr(D2[var]))
cg = float(G["f_tot_mid"].corr(G[var])) if var in G.columns else np.nan
print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [ingressi n={len(D2)}]"
+ (f" {cg:+.3f} [panel n={len(G)}]" if np.isfinite(cg) else ""))
print(" (positivo = il credito reale si avvicina al modello quando lo skew e' PIATTO;")
print(" e' il verso atteso dal meccanismo, ma n=22 e il panel e' autocorrelato.)")
print("\n IL PEZZO PIU' GRANDE (f_term) CAMBIA SEGNO COL REGIME — ed e' questo che decide")
print(" se 0.73 e' conservativo o ottimista dove VRP01 tradera' DAVVERO.")
G2 = G.copy()
G2["contango"] = G2["atm7"] < G2["dvol"]
print(f" ore del panel con ATM7 < DVOL30 (contango, il modello SOVRAPPREZZA): "
f"{G2['contango'].mean():.1%}")
for nome, m in (("contango (ATM7<DVOL)", G2["contango"]), ("backwardation (ATM7>DVOL)", ~G2["contango"])):
g = G2[m]
if g.empty:
continue
print(f" {nome:26s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | "
f"f_skew {g['f_skew'].median():.3f} | f_tot {g['f_tot'].median():.3f} | "
f"IV-rank mediano {g['ivrank'].median():.3f}")
qs = pd.qcut(G2["ivrank"], 4, labels=["q1", "q2", "q3", "q4"], duplicates="drop")
st = G2.groupby(qs, observed=True).agg(n=("f_term", "size"), ivr=("ivrank", "median"),
f_term=("f_term", "median"), f_skew=("f_skew", "median"),
f_tot=("f_tot", "median"))
print(" stratificato per quartile di IV-rank (DENTRO il campione: max 0.23, il gate e' 0.30):")
print(st.round(3).to_string())
print(" -> f_term sale con l'IV-rank; f_skew no. L'estrapolazione oltre 0.30 NON e' misurata")
print(" e in questo campione non e' misurabile: l'unico episodio ad alta vol e' un RALLY")
print(" (2026-08-21, +26% in una settimana), non un crash. Manca il regime che conta.")
print(f"\n IV-rank agli ingressi: min {D['ivrank'].min():.3f} mediana {D['ivrank'].median():.3f} "
f"max {D['ivrank'].max():.3f}")
n_gate = int((D["ivrank"] > 0.30).sum())
print(f" ingressi che passerebbero il gate IV-rank>0.30 di VRP01: {n_gate}/{len(D)}")
print(" -> il f di VRP01 e' misurato INTERAMENTE nel regime in cui il sleeve sta FLAT.")
print("\n Tabella per ingresso:")
cols = ["asset", "ts", "dte", "ivrank", "dvol", "atm7", "rr7", "iv_short", "iv_long",
"f_term", "f_skew", "f_markfit", "f_spread", "f_tot_mid", "f_tot"]
P = D[cols].copy()
P["ts"] = pd.DatetimeIndex(P["ts"]).strftime("%Y-%m-%d %H:%M")
for c in ("f_term", "f_skew", "f_markfit", "f_spread", "f_tot_mid", "f_tot"):
P[c] = P[c].round(3)
print(P.to_string(index=False))
# ------------------------------------------------------------ 5. SOMMARIO
rule("5. SOMMARIO — cosa regge, cosa no, e cosa non e' misurabile con 75 giorni")
print("""
Q1 lo skew NON anticipa lo spot: e' lo spot che muove lo skew.
Il prezzo precede il RR con t da 3.1 a 9.4 su 8 test su 8 e su entrambi gli asset;
in avanti il |t| massimo su 16 test e' 2.35 contro un massimo atteso dal rumore di 2.08,
non replica sul secondo asset, e l'unica cella apparente perde i due terzi togliendo
4 giorni di rally su 24. La strategia giornaliera migliore su 18 celle fa Sharpe +2.61
con IC95% [-2.90, +5.86] e DSR 0.163 -> SCARTATO.
Q2 gate di rischio: NON MISURABILE su questa finestra, e la ragione e' strutturale.
Nei 75 giorni TP01 e' attivo 5 giorni su 75 e il suo maxDD e' 0.14%: non c'e' sinistro
da prevenire. Cio' che si vede: alle soglie z<-1.0 e z<-1.5 il gate spegne giorni in cui
TP01 era GIA' flat nel 100% dei casi -> 5a conferma della ridondanza col trend, ma su un
campione senza potenza. Il null de-levering gira e dice REFUTED per assenza di DD, non
per merito del baseline.
Q3 il risultato della sessione, e non e' una strategia: e' una CORREZIONE DI PREZZATURA.
Il f=0.73 di VRP01 e' replicato in modo indipendente (0.714 su 22 ingressi, 0.723 su 2.582
ore) e poi SCOMPOSTO. La causa maggiore NON e' lo skew:
struttura a termine 42% | spread denaro-lettera 30% | skew 25% | fit 2%
Il sleeve prezza un'opzione a 7 giorni con il DVOL a 30 giorni, che nel 90% delle ore sta
~3 punti di vol SOPRA l'ATM a 7 giorni -> sovrapprezza entrambe le gambe. Il "+0.8pp sulla
gamba corta" pubblicato il 30/07 (qui +0.20pp) e' la CANCELLAZIONE di due errori da ~3pp:
-2.97pp di termine e +2.79pp di skew. Guardando le gambe contro l'ATM a 7 giorni si vede
che il modello sbaglia su ENTRAMBE, non solo sull'ala.
E il pezzo grande CAMBIA SEGNO col regime: f_term 0.83 nel quartile basso di IV-rank ->
0.93 nel quartile alto, e 1.05 nelle ore in backwardation, mentre f_skew resta piatto
(0.93 -> 0.92). Cioe' l'errore piu' grande si annulla proprio dove il gate IV-rank fa
tradare il sleeve.
""")
rule(f"fine — {time.time()-t_start:.0f}s")
if __name__ == "__main__":
main()
+591
View File
@@ -0,0 +1,591 @@
"""r0822 SLIP-AUDIT — l'attrito VERO del libro live: quanto costa davvero un fill.
DOMANDA
-------
Il progetto modella le fee con cura chirurgica (3,50 bps/lato misurati sui fill reali il 21/08)
ma ASSUME il fill al prezzo di chiusura della barra. Lo **slippage realizzato** del libro live
non e' mai stato misurato: e' dichiarato "rischio #1" per XSR01 e semplicemente ignorato per
TP01/SKH01. Ci sono 18 esecuzioni reali dal 2026-07-14. Questo script le misura.
La domanda decidibile e' UNA: i backtest girano a `fee_rt=0.001` (10 bps RT) mentre la fee vera
e' 3,50 bps/lato = 7 bps RT. C'e' **3 bps RT = 1,50 bps/LATO** di margine gia' pagato dal modello.
**Lo slippage realizzato ci sta dentro?**
TRE DIFETTI DEL DATO TROVATI PRIMA DI MISURARE (e riparati/dichiarati qui)
-------------------------------------------------------------------------
(1) **`ts_utc` nel log NON e' l'ora del fill.** `book_execute.py:213` scrive
`ts_utc=str(pd.Timestamp(r['last_data']))` = l'etichetta dell'ULTIMA BARRA DEL FEED, non
l'orologio. Tutti e 18 i record dicono `00:00:00`. L'ora vera si recupera solo dalle
intestazioni di `logs/cron_book.log` (`===== <ISO> cron_book =====`). Senza questo passo
ogni confronto "al prezzo dell'ora del fill" misurerebbe la mezzanotte.
Prova che serviva: il fill #1 e' etichettato `2026-07-08` ed e' stato eseguito il
**2026-07-14T14:00Z** — sei giorni dopo (feed-freeze del 14/07, gia' noto al progetto).
(2) **Il feed certificato NON e' lo strumento che si trada.** `rebuild_history.DERIBIT_INSTR`
prende `BTC/USD:BTC` / `ETH/USD:ETH` = i perpetual **INVERSE**; `deribit.INSTRUMENT` esegue su
`BTC_USDC-PERPETUAL` / `ETH_USDC-PERPETUAL` = i **LINEARI USDC**. Il commento nel sorgente dice
"inverse ~ lineare entro 3 bps": **3 bps e' esattamente la taglia della domanda**. Misurare lo
slippage contro il feed certificato significherebbe misurare la BASE, non lo slippage.
Qui la base viene MISURATA (non assunta) e il riferimento vero e' lo strumento tradato.
Stessa forma del difetto di `fee_watch` del 21/08.
(3) **Convenzione LINEARE**: `amount` = quantita' BASE, `fee` gia' in USDC ->
`fee_bps = fee / (filled * price) * 1e4`. La convenzione inverse darebbe ~8 ordini di
grandezza di errore SENZA sollevare nulla. Il controllo e' stampato in chiaro.
TRE RIFERIMENTI, TENUTI SEPARATI (il punto 1c della missione)
------------------------------------------------------------
Il modello decide sulla barra GIORNALIERA, il cron esegue all'ORA: sono grandezze diverse.
REF-A "modello" = close della barra 1d etichettata `last_data` (= il prezzo a cui il
BACKTEST assume di riempire). Misura modello-vs-realta', NON slippage.
REF-B "5m/1h" = close dell'ultima barra CHIUSA prima del fill sullo strumento TRADATO.
Questo, e solo questo, e' slippage d'esecuzione.
REF-C "VWAP 1h" = VWAP delle 12 barre 5m dell'ora che contiene il fill (benchmark
NON causale, dichiarato tale).
Segno orientato al lato: `costo_bps = lato * (fill - ref)/ref * 1e4`, lato +1 buy / -1 sell.
Positivo = costo, negativo = guadagno.
LA STATISTICA CHE HA POTENZA (e perche' quella in bps non ne ha)
----------------------------------------------------------------
Con n=18 la via in bps e' persa in partenza: il rumore e' la deriva del prezzo fra la chiusura
della barra 5m e il fill, che vale decine di bps. La statistica giusta e' NORMALIZZATA dal range
della barra: il nostro fill e' esso stesso un print della barra 5m che lo contiene, quindi
q = (fill - low)/(high - low) per un BUY, q = 1 - quello per un SELL.
Se i nostri ordini market avessero impatto, un BUY stamperebbe AL MASSIMO della barra -> q -> 1.
Senza impatto E[q] = 0.5, e il null NON si assume uniforme: si MISURA su ~18.000 barre vere.
⚠️ ESITO SULLA MIA STESSA POTENZA: assumendo l'uniforme avevo dichiarato MDE_q = 0.19 di range
(~5 bps). Il null misurato e' 1.43x piu' largo (il close di una barra 5m sta spesso proprio sul
proprio estremo) -> MDE vera 0.27 = ~7.2 bps, contro 7.8 della via in bps. **La normalizzazione
non compra potenza utile.** Con n=18 nessuna via statistica risolve 1,5 bps: cio' che decide e'
l'argomento STRUTTURALE (tick, partecipazione al nastro, e l'esperimento naturale del fill #1).
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822_slip_audit.py
(--no-net salta il fetch dello strumento lineare e lo dichiara non misurato)
"""
from __future__ import annotations
import json
import os
import re
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))
SCRATCH = Path(os.environ.get("SLIP_SCRATCH", "/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad"))
EXEC_LOG = ROOT / "data" / "live" / "book_executions.jsonl"
CRON_LOG = ROOT / "logs" / "cron_book.log"
# Sensibilita' PUBBLICATA del libro (r0726_fee_sensitivity.py, CLAUDE.md 26/07).
# Unita': per bps PER LATO (verificato sulla tabella: 0->15 bps/lato = Sharpe 1.849->1.597).
DSHARPE_PER_BPS_SIDE = -0.017
DCAGR_PER_BPS_SIDE = -0.23 # punti percentuali di CAGR
# Margine gia' pagato dal modello: backtest 10 bps RT vs fee vera 7 bps RT = 3 bps RT.
MARGIN_BPS_SIDE = 1.5
FEE_TAKER_BPS_SIDE = 3.50 # tier base misurato (fee_watch, 21/08)
# Strumento TRADATO (src/live/deribit.INSTRUMENT) vs strumento del FEED certificato
# (scripts/analysis/rebuild_history.DERIBIT_INSTR). NON sono lo stesso: vedi difetto (2).
CCXT_LINEAR = {"BTC": "BTC/USDC:USDC", "ETH": "ETH/USDC:USDC"}
CCXT_INVERSE = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"}
RNG = np.random.default_rng(822)
N_BOOT = 20_000
# ----------------------------------------------------------------------------- fills
def _parse_cron_fills() -> list[dict]:
"""Ora VERA di ogni fill, dalle intestazioni di cron_book.log. Vedi difetto (1)."""
if not CRON_LOG.exists():
raise SystemExit(f"manca {CRON_LOG}: l'ora vera del fill non e' recuperabile altrove")
head = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book =====")
fill = re.compile(r"^\s+-> (BUY|SELL) ([\d.]+) @ \$([\d,.]+) fee ([\d.]+) \((\w+)\)")
out, cur = [], None
with open(CRON_LOG, errors="replace") as f:
for line in f:
m = head.match(line)
if m:
cur = pd.Timestamp(m.group(1))
continue
m = fill.match(line)
if m:
out.append(dict(fill_ts=cur, side=m.group(1).lower(),
amt=float(m.group(2)), px=float(m.group(3).replace(",", "")),
fee=float(m.group(4)), status=m.group(5)))
return out
def load_fills() -> pd.DataFrame:
"""JSONL (verita' sui numeri) + cron log (verita' sull'ora), appaiati e VERIFICATI."""
recs = [json.loads(l) for l in EXEC_LOG.read_text().splitlines() if l.strip()]
cron = _parse_cron_fills()
if len(recs) != len(cron):
raise SystemExit(f"appaiamento impossibile: {len(recs)} righe jsonl vs {len(cron)} fill "
f"nel cron log. Non invento un'ora: mi fermo.")
rows = []
for i, (r, c) in enumerate(zip(recs, cron)):
# entrambi i file sono append-only dallo stesso processo -> stesso ordine. Lo PROVO.
ok = (r["side"] == c["side"]
and abs(round(r["filled"], 4) - round(c["amt"], 4)) < 1e-9
and abs(round(r["price"], 1) - round(c["px"], 1)) < 0.051
and abs(round(r["fee"], 5) - round(c["fee"], 5)) < 1e-9)
if not ok:
raise SystemExit(f"riga {i}: jsonl e cron log NON combaciano ({r} vs {c}) -> mi fermo")
if c["status"] != "OK":
continue
rows.append(dict(
i=i, fill_ts=c["fill_ts"], model_bar=pd.Timestamp(r["ts_utc"], tz="UTC"),
asset=r["asset"], side=r["side"], lato=1.0 if r["side"] == "buy" else -1.0,
filled=float(r["filled"]), price=float(r["price"]), fee=float(r["fee"]),
notional=float(r["filled"]) * float(r["price"]),
skh=r.get("skh_sign"), tp_frac=r.get("tp_frac"),
))
return pd.DataFrame(rows)
# ----------------------------------------------------------------------------- prezzi
def _certified(asset: str, tf: str, t0, t1) -> pd.DataFrame:
"""Feed certificato su disco = strumento INVERSE. Letto a colonne e tagliato subito (RAM)."""
f = ROOT / "data" / "raw" / f"{asset.lower()}_{tf}.parquet"
d = pd.read_parquet(f, columns=["timestamp", "open", "high", "low", "close", "volume"])
d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d = d[(d["ts"] >= t0) & (d["ts"] <= t1)].drop(columns=["timestamp"]).reset_index(drop=True)
return d
def _fetch_linear(asset: str, t0, t1) -> pd.DataFrame | None:
"""Strumento TRADATO (lineare USDC) via ccxt pubblico. Cache su scratchpad, pacing gentile.
Il progetto ha una storia di rate-limit per-IP (29-30/07): qui sono ~13 pagine per asset,
distanziate. Nessuna scrittura sotto data/."""
SCRATCH.mkdir(parents=True, exist_ok=True)
cache = SCRATCH / f"lin_{asset.lower()}_5m.parquet"
if cache.exists():
d = pd.read_parquet(cache)
if d["ts"].min() <= t0 and d["ts"].max() >= t1 - pd.Timedelta(minutes=10):
return d
try:
import ccxt, time
ex = ccxt.deribit({"enableRateLimit": True})
sym, since, rows = CCXT_LINEAR[asset], int(t0.timestamp() * 1000), []
end = int(t1.timestamp() * 1000)
while since < end:
o = ex.fetch_ohlcv(sym, "5m", since=since, limit=1000)
if not o:
break
rows += o
since = o[-1][0] + 300_000
time.sleep(0.35)
if not rows:
return None
d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
d = d.drop_duplicates("timestamp").sort_values("timestamp")
d["ts"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d = d.drop(columns=["timestamp"]).reset_index(drop=True)
d.to_parquet(cache)
return d
except Exception as e: # rete assente = si dichiara
print(f" ! fetch lineare {asset} fallito: {type(e).__name__}: {str(e)[:90]}")
return None
def _last_closed(bars: pd.DataFrame, t) -> pd.Series | None:
"""Ultima barra CHIUSA a `t`: una barra etichettata b chiude a b+durata."""
if bars is None or bars.empty:
return None
step = bars["ts"].diff().median()
ok = bars[bars["ts"] + step <= t]
return None if ok.empty else ok.iloc[-1]
def _containing(bars: pd.DataFrame, t) -> pd.Series | None:
if bars is None or bars.empty:
return None
step = bars["ts"].diff().median()
ok = bars[(bars["ts"] <= t) & (t < bars["ts"] + step)]
return None if ok.empty else ok.iloc[-1]
# ----------------------------------------------------------------------------- stats
def ci_mean(x, boot=True):
x = np.asarray(x, float)
x = x[np.isfinite(x)]
n = len(x)
if n < 2:
return dict(n=n, mean=np.nan, lo=np.nan, hi=np.nan, sd=np.nan, med=np.nan)
m, sd = x.mean(), x.std(ddof=1)
if boot:
bs = RNG.choice(x, size=(N_BOOT, n), replace=True).mean(axis=1)
lo, hi = np.percentile(bs, [2.5, 97.5])
else:
from scipy import stats
h = stats.t.ppf(0.975, n - 1) * sd / np.sqrt(n)
lo, hi = m - h, m + h
return dict(n=n, mean=m, lo=lo, hi=hi, sd=sd, med=float(np.median(x)))
def fmt(d, u="bps"):
if not np.isfinite(d["mean"]):
return "n/d"
z = "" if (d["lo"] <= 0 <= d["hi"]) else " <- ZERO ESCLUSO"
return f"{d['mean']:+7.2f} {u} IC95 [{d['lo']:+7.2f}, {d['hi']:+7.2f}] (med {d['med']:+7.2f}, n={d['n']}){z}"
def main():
print("=" * 96)
print(" r0822 SLIP-AUDIT — l'attrito VERO del libro live (18 esecuzioni, dal 2026-07-14)")
print("=" * 96)
fills = load_fills()
t0 = fills["fill_ts"].min().floor("D") - pd.Timedelta(days=3)
t1 = fills["fill_ts"].max().ceil("h") + pd.Timedelta(hours=2)
# ---------------------------------------------------------------- 0. difetti del dato
print("\n[0] DIFETTI DEL DATO — trovati PRIMA di misurare\n" + "-" * 96)
lag = (fills["fill_ts"] - fills["model_bar"]).dt.total_seconds() / 3600.0
print(f" (1) `ts_utc` nel jsonl = etichetta dell'ultima barra del feed, NON l'ora del fill.")
print(f" 18/18 record dicono 00:00:00. Ora vera recuperata da logs/cron_book.log.")
print(f" Scarto fill - etichetta: mediana {lag.median():.1f}h, max {lag.max():.1f}h "
f"({(lag > 26).sum()} fill oltre le 26h).")
if (lag > 26).any():
w = fills.loc[lag.idxmax()]
print(f" Il caso estremo e' il fill #{w['i']}: etichetta {w['model_bar'].date()}, "
f"eseguito {w['fill_ts']} = feed-freeze del 14/07 (gia' noto al progetto).")
print(f" (2) Feed certificato = INVERSE ({CCXT_INVERSE['BTC']}), libro tradato = LINEARE "
f"({CCXT_LINEAR['BTC']}).")
print(f" La base fra i due e' misurata sotto, non assunta.")
# ---------------------------------------------------------------- 1. convenzione fee
print("\n[1] CONTROLLO DI CONVENZIONE (lineare USDC) — la trappola da ~8 ordini di grandezza\n"
+ "-" * 96)
fills["fee_bps"] = fills["fee"] / fills["notional"] * 1e4
fb = ci_mean(fills["fee_bps"])
print(f" fee_bps = fee_usdc / (filled * price) * 1e4 [LINEARE: amount=base, fee gia' in USDC]")
print(f" media {fb['mean']:.3f} bps/lato · mediana {fb['med']:.3f} · "
f"min {fills['fee_bps'].min():.3f} · max {fills['fee_bps'].max():.3f}")
print(f" tier base dichiarato da fee_watch: {FEE_TAKER_BPS_SIDE:.2f} bps/lato -> "
f"scarto {fb['med'] - FEE_TAKER_BPS_SIDE:+.3f} bps")
bad = (fills["fee"] / fills["filled"] * 1e4).median()
print(f" [se avessi usato la convenzione INVERSE: {bad:,.0f} bps -> "
f"{bad / FEE_TAKER_BPS_SIDE:,.0f}x il vero, e senza sollevare nulla]")
# Il nuovo schema fee Deribit e' entrato il 2026-08-01. La tabella `_CONTRACT`/fee_watch
# legge il tier dichiarato dal venue; qui lo si legge sul NASTRO, che e' la prova migliore.
CUT = pd.Timestamp("2026-08-01", tz="UTC")
pre, post = fills[fills["fill_ts"] < CUT], fills[fills["fill_ts"] >= CUT]
print(f" ripartito sul cambio di listino del 2026-08-01:")
for nm, g in (("PRIMA ", pre), ("DOPO ", post)):
if len(g):
u = sorted(float(x) for x in g["fee_bps"].round(2).unique())
print(f" {nm} n={len(g):2d} fee/lato {u} "
f"(nozionale ${g['notional'].min():.0f}-${g['notional'].max():.0f})")
if len(pre) and len(post):
print(f" => il taglio {pre['fee_bps'].median():.2f} -> {post['fee_bps'].median():.2f} "
f"bps/lato e' VISIBILE sul nastro, {len(post)}/{len(post)} fill esatti al nuovo tier.")
print(f" Non e' un effetto taglia (i due fill a {pre['fee_bps'].median():.2f} bps sono da "
f"${pre['notional'].median():.0f}, sopra la mediana del campione).")
print(f" Conferma INDIPENDENTE di fee_watch (che legge il tier dichiarato, non i fill)")
print(f" e della regola pre-registrata del 26/07: taker <=5 bps/lato -> non si tocca")
print(f" nulla. Il listino si e' mosso NEL VERSO BUONO.")
# ---------------------------------------------------------------- 2. potenza, PRIMA
print("\n[2] POTENZA DICHIARATA — calcolata PRIMA di guardare lo slippage\n" + "-" * 96)
sig5 = {}
for a in sorted(fills["asset"].unique()):
d = _certified(a, "5m", t0, t1)
r = np.log(d["close"]).diff().dropna() * 1e4
sig5[a] = float(r.std())
sig = float(np.mean(list(sig5.values())))
n = len(fills)
mde_bps = 2.80 * sig / np.sqrt(n) # 80% potenza, bilaterale alpha=0.05
print(f" Rumore strutturale: sigma dei ritorni 5m nella finestra = "
+ " · ".join(f"{a} {v:.1f} bps" for a, v in sig5.items()) + f" (media {sig:.1f})")
print(f" Il riferimento 5m dista dal fill 0-5 min -> il rumore e' di QUELL'ordine.")
print(f" MDE in bps a n={n}, potenza 80%, alpha 5%: 2.80 * {sig:.1f} / sqrt({n}) = "
f"**{mde_bps:.1f} bps/lato**")
print(f" Il margine da difendere e' {MARGIN_BPS_SIDE:.1f} bps/lato (3 bps RT).")
print(f" => VERDETTO DI POTENZA, dichiarato prima del risultato: la via in bps NON puo'")
print(f" risolvere {MARGIN_BPS_SIDE:.1f} bps ({mde_bps / MARGIN_BPS_SIDE:.0f}x troppo grossolana). "
f"Qualunque IC in bps conterra' lo zero;")
print(f" un 'non distinguibile da zero' li' NON e' una prova di assenza di slippage.")
mde_q = 2.80 * (1 / np.sqrt(12)) / np.sqrt(n)
print(f" Statistica normalizzata q (posizione del fill nella barra 5m che lo contiene):")
print(f" sigma_q = 1/sqrt(12) = 0.289 -> MDE_q = {mde_q:.3f} di range di barra.")
print(f" E' la sola con potenza utile, e ha un'interpretazione fisica: un market order con")
print(f" impatto stampa AL MASSIMO della barra (q->1), perche' il nostro print E' nella barra.")
# ---------------------------------------------------------------- 3. base inverse/lineare
print("\n[3] BASE inverse-vs-lineare — MISURATA, non assunta (difetto 2)\n" + "-" * 96)
lin, inv = {}, {}
use_net = "--no-net" not in sys.argv
for a in sorted(fills["asset"].unique()):
inv[a] = _certified(a, "5m", t0, t1)
lin[a] = _fetch_linear(a, t0, t1) if use_net else None
basis_ok = all(lin[a] is not None for a in lin)
if basis_ok:
for a in sorted(lin):
m = pd.merge(inv[a][["ts", "close"]], lin[a][["ts", "close"]], on="ts",
suffixes=("_inv", "_lin"))
b = (m["close_lin"] - m["close_inv"]) / m["close_inv"] * 1e4
print(f" {a}: n={len(m):,} barre 5m · base mediana {b.median():+.2f} bps · "
f"media {b.mean():+.2f} · IQR [{b.quantile(.25):+.2f}, {b.quantile(.75):+.2f}] · "
f"|base| p95 {b.abs().quantile(.95):.2f}")
print(f" => Il commento nel sorgente ('inverse ~ lineare entro 3 bps') e' CONFERMATO come")
print(f" ordine di grandezza, ed e' esattamente la taglia del margine da difendere:")
print(f" misurare lo slippage sul feed certificato avrebbe misurato la BASE.")
else:
print(" NON MISURATA (nessuna rete / --no-net): i riferimenti sullo strumento tradato")
print(" non sono disponibili e i numeri sotto restano sul feed INVERSE = contaminati.")
# ---------------------------------------------------------------- 4. i tre riferimenti
print("\n[4] I TRE RIFERIMENTI — segno orientato al lato (positivo = COSTO)\n" + "-" * 96)
inv1h = {a: _certified(a, "1h", t0 - pd.Timedelta(days=10), t1)
for a in sorted(fills["asset"].unique())}
daily = {}
for a, d in inv1h.items():
g = d.set_index("ts").resample("1D")
daily[a] = pd.DataFrame(dict(close=g["close"].last())).dropna()
rows = []
for _, f in fills.iterrows():
a, t, s, px = f["asset"], f["fill_ts"], f["lato"], f["price"]
r = dict(f)
# REF-A: close della barra 1d etichettata last_data = il fill che il BACKTEST assume
dd = daily[a]
key = f["model_bar"].tz_convert("UTC").normalize()
r["ref_A"] = float(dd.loc[key, "close"]) if key in dd.index else np.nan
src = lin[a] if basis_ok else inv[a]
b5 = _last_closed(src, t)
r["ref_B5"] = float(b5["close"]) if b5 is not None else np.nan
# barra 5m CONTENENTE il fill -> statistica q
cb = _containing(src, t)
if cb is not None and cb["high"] > cb["low"]:
p = (px - cb["low"]) / (cb["high"] - cb["low"])
r["q"] = p if s > 0 else 1.0 - p
r["in_bar"] = bool(cb["low"] - 1e-9 <= px <= cb["high"] + 1e-9)
r["bar_rng_bps"] = (cb["high"] - cb["low"]) / cb["close"] * 1e4
# quota della NOSTRA size sul volume della barra che ci contiene: se siamo
# una frazione trascurabile del nastro, l'impatto e' strutturalmente implausibile.
r["partecip"] = f["filled"] / cb["volume"] if cb["volume"] > 0 else np.nan
else:
r["q"], r["in_bar"], r["bar_rng_bps"], r["partecip"] = np.nan, None, np.nan, np.nan
# REF-B1h e REF-C VWAP dell'ora contenente
h0 = t.floor("h")
hb = src[(src["ts"] >= h0) & (src["ts"] < h0 + pd.Timedelta(hours=1))]
r["ref_C"] = float((hb["close"] * hb["volume"]).sum() / hb["volume"].sum()) \
if len(hb) and hb["volume"].sum() > 0 else np.nan
prev = src[src["ts"] + pd.Timedelta(minutes=5) <= h0]
r["ref_B1"] = float(prev.iloc[-1]["close"]) if len(prev) else np.nan
for k in ("A", "B5", "B1", "C"):
v = r.get(f"ref_{k}")
r[f"slip_{k}"] = s * (px - v) / v * 1e4 if v and np.isfinite(v) else np.nan
rows.append(r)
R = pd.DataFrame(rows)
lab = {"A": "REF-A barra 1d del modello (= fill assunto dal BACKTEST)",
"B5": "REF-B5 close ultima barra 5m CHIUSA (= SLIPPAGE d'esecuzione)",
"B1": "REF-B1 close ultima barra 1h CHIUSA",
"C": "REF-C VWAP dell'ora contenente (non causale)"}
strumento = "LINEARE (tradato)" if basis_ok else "INVERSE (certificato) — CONTAMINATO da base"
print(f" riferimenti B5/B1/C calcolati sullo strumento {strumento}\n")
for k in ("A", "B5", "B1", "C"):
print(f" {lab[k]}\n {fmt(ci_mean(R[f'slip_{k}']))}")
print(f"\n ⚠️ REF-A NON e' slippage e il suo SEGNO non e' un guadagno. La barra 1d etichettata")
print(f" `last_data` chiude fino a ~23h DOPO il fill: su questa finestra (BTC 66k->78k in")
print(f" 4 giorni) un trend-follower che compra alle 01:07 batte per costruzione la")
print(f" chiusura serale, ma il backtest tiene la posizione DA quella chiusura — e' una")
print(f" differenza di FASE, non un attrito. La domanda 'quanto costa la barra parziale'")
print(f" e' gia' stata misurata bene il 26/07 (r0726_tp01_partial_day, 24 ancore appaiate:")
print(f" ±0.03 di Sharpe per TP01): n=18 su 5 settimane non la migliora e non la smentisce.")
print(f"\n Per asset e per lato (REF-B5, lo slippage vero):")
for a in sorted(R["asset"].unique()):
for sd in ("buy", "sell"):
x = R[(R["asset"] == a) & (R["side"] == sd)]["slip_B5"]
if len(x):
print(f" {a} {sd:4s} n={len(x):2d} media {x.mean():+7.2f} med {x.median():+7.2f}")
# La media di REF-B5 e' dominata da UN giorno: la si mostra con e senza, e si dice quale
# si cita. Non e' cherry-picking se si pubblicano entrambe e la regola e' dichiarata.
wild = R.nlargest(3, "bar_rng_bps")
day = wild["fill_ts"].dt.date.mode()[0]
keep = R[R["fill_ts"].dt.date != day]
print(f"\n ROBUSTEZZA: le 3 barre piu' larghe del campione ({wild['bar_rng_bps'].min():.0f}-"
f"{wild['bar_rng_bps'].max():.0f} bps di range) cadono tutte il {day}")
print(f" (ETH +14% in 6 ore). Sono {len(R) - len(keep)} fill su {len(R)} e da soli fanno")
print(f" la media: REF-B5 su tutto {ci_mean(R['slip_B5'])['mean']:+.2f} bps -> "
f"{ci_mean(keep['slip_B5'])['mean']:+.2f} bps senza quel giorno")
print(f" (IC95 [{ci_mean(keep['slip_B5'])['lo']:+.2f}, {ci_mean(keep['slip_B5'])['hi']:+.2f}], "
f"n={len(keep)}). La MEDIANA invece non si muove: "
f"{R['slip_B5'].median():+.2f} -> {keep['slip_B5'].median():+.2f}.")
print(f" => con n=18 la media in bps e' una statistica inservibile (un giorno la")
print(f" ribalta); si cita la mediana, e comunque decide l'argomento strutturale.")
# ---------------------------------------------------------------- 5. la statistica con potenza
print("\n[5] LA STATISTICA CON POTENZA — q, posizione del fill nella barra 5m\n" + "-" * 96)
q = R["q"].dropna()
dq = ci_mean(q)
print(f" q = (fill-low)/(high-low) per i BUY, 1-quello per i SELL. E[q]=0.5 senza impatto.")
print(f" media q {dq['mean']:.3f} IC95 [{dq['lo']:.3f}, {dq['hi']:.3f}] "
f"(med {dq['med']:.3f}, n={dq['n']})")
print(f" scostamento da 0.5: {dq['mean'] - 0.5:+.3f} (MDE dichiarata {mde_q:.3f})")
outside = (R["in_bar"] == False).sum()
print(f" fill FUORI dal range della propria barra: {outside}/{dq['n']} "
f"(un fill fuori sarebbe strumento o ora sbagliati)")
hi_obs, lo_obs = int((q >= 0.99).sum()), int((q <= 0.01).sum())
print(f" fill sull'ESTREMO avverso (q>=0.99 = firma di impatto): {hi_obs}/{dq['n']}"
f" · sull'estremo favorevole (q<=0.01): {lo_obs}/{dq['n']}")
rng_med = float(R["bar_rng_bps"].median())
print(f" range mediano di barra 5m: {rng_med:.1f} bps -> lo scostamento misurato vale")
print(f" ~{(dq['mean'] - 0.5) * rng_med:+.2f} bps/lato "
f"[IC95 {(dq['lo'] - 0.5) * rng_med:+.2f}, {(dq['hi'] - 0.5) * rng_med:+.2f}]")
# -- null EMPIRICO: "0.5" e' un'assunzione (uniforme). Qui il null e' MISURATO sulle barre
# vere: dove sta il close di una barra 5m dentro il proprio range? Segno randomizzato
# per imitare l'orientamento al lato.
qc = []
for a in sorted(fills["asset"].unique()):
src = lin[a] if basis_ok else inv[a]
m = src[src["high"] > src["low"]]
qc.append(((m["close"] - m["low"]) / (m["high"] - m["low"])).to_numpy())
qc = np.concatenate(qc)
flip = RNG.random((N_BOOT, dq["n"])) < 0.5
draw = RNG.choice(qc, size=(N_BOOT, dq["n"]), replace=True)
null = np.where(flip, draw, 1.0 - draw).mean(axis=1)
pval = float((null >= dq["mean"]).mean())
print(f"\n NULL EMPIRICO (non 'uniforme per assunzione'): posizione del close nel proprio")
print(f" range, su {len(qc):,} barre 5m vere, segno randomizzato.")
sd_unif = (1 / np.sqrt(12)) / np.sqrt(dq["n"])
print(f" null: media {null.mean():.3f} · sd {null.std():.3f} "
f"(l'uniforme darebbe sd {sd_unif:.3f})")
print(f" p(one-sided, impatto = q piu' alto del null) = {pval:.3f}"
f" -> {'IMPATTO RILEVATO' if pval < 0.05 else 'nessun impatto rilevabile'}")
# ...e quanti estremi ci si ASPETTA sotto quel null (un conteggio nudo non e' un'evidenza)
p_hi = float(((qc >= 0.99).mean() + (qc <= 0.01).mean()) / 2)
print(f" attesi sotto il null: {p_hi * dq['n']:.1f} su {dq['n']} per lato "
f"(osservati {hi_obs} avversi / {lo_obs} favorevoli)")
print(f" -> l'unico q>=0.99 e' il fill #3 (ETH sell, q=0.996) su una barra da 7.5 bps di")
print(f" range: e' 0.03 bps di prezzo. Un estremo su una barra piatta non e' impatto.")
mde_q_true = 2.80 * float(null.std())
print(f"\n ⚠️ CORREZIONE ALLA MIA STESSA POTENZA DICHIARATA. Il null vero e' "
f"{null.std() / sd_unif:.2f}x piu' largo")
print(f" dell'uniforme (il close di una barra 5m sta spesso PROPRIO sul suo estremo:")
print(f" sd per-osservazione {null.std() * np.sqrt(dq['n']):.3f} contro 0.289 dell'uniforme).")
print(f" MDE_q vera = {mde_q_true:.3f} di range = {mde_q_true * rng_med:.1f} bps, non "
f"{mde_q * rng_med:.1f}: la mia stima")
print(f" a priori era ottimista del {(mde_q_true / mde_q - 1) * 100:.0f}%. => la statistica")
print(f" normalizzata NON compra potenza utile ({mde_q_true * rng_med:.1f} bps contro "
f"{mde_bps:.1f} della via in bps):")
print(f" con n=18 NESSUNA via statistica risolve {MARGIN_BPS_SIDE:.1f} bps. Cio' che decide")
print(f" e' l'argomento strutturale qui sotto, non un intervallo di confidenza.")
# -- argomento FISICO, indipendente dal campione: quanto siamo grandi e quanto vale un tick
from src.live.deribit import _CONTRACT, INSTRUMENT
print(f"\n ARGOMENTO STRUTTURALE (non dipende da n=18):")
pr = R["partecip"].dropna()
print(f" la nostra size sul volume della barra 5m che ci contiene: mediana "
f"{pr.median() * 100:.2f}% · p90 {pr.quantile(.9) * 100:.2f}% · max {pr.max() * 100:.2f}%")
top = R.dropna(subset=["partecip"]).nlargest(1, "partecip").iloc[0]
print(f"\n ESPERIMENTO NATURALE — il fill #{top['i']} ({top['fill_ts']:%Y-%m-%d %H:%M} "
f"{top['fill_ts']:%a}, {top['asset']} ${top['notional']:.0f}):")
print(f" e' stato il {top['partecip'] * 100:.1f}% del volume della propria barra 5m")
print(f" (weekend, nastro sottilissimo) e quella barra ha avuto un range di SOLI")
print(f" {top['bar_rng_bps']:.2f} bps, con il nostro print a meta' (q={top['q']:.2f}).")
print(f" Un ordine che prende un quinto del nastro e lascia il prezzo entro 1 bps e'")
print(f" la prova diretta che a questa taglia il libro NON cammina il book. Vale piu'")
print(f" dell'intero intervallo di confidenza: e' il caso peggiore del campione.")
print(f" ⚠️ ed e' un SABATO: la partecipazione peggiore cade nel weekend, dove il")
print(f" progetto ha misurato che TP01 fa il 38% del proprio gross. Se l'attrito")
print(f" mordera' a taglia maggiore, mordera' proprio dove sta il rendimento.")
for a in sorted(R["asset"].unique()):
tick = _CONTRACT[INSTRUMENT[a]]["tick"]
px = float(R[R["asset"] == a]["price"].median())
print(f" {a}: tick {tick} su ~${px:,.0f} = {tick / px * 1e4:.4f} bps -> anche uno")
print(f" spread di 5 tick costerebbe {2.5 * tick / px * 1e4:.3f} bps di mezzo-spread")
print(f" => il pavimento fisico del costo d'esecuzione e' ~0.1 bps, DUE ordini di")
print(f" grandezza sotto il margine di {MARGIN_BPS_SIDE:.1f} bps. Perche' lo slippage")
print(f" sfondi il margine il libro dovrebbe CAMMINARE il book — e a <1% del nastro,")
print(f" con MENO estremi avversi di quanti il null ne preveda ({hi_obs} contro "
f"{p_hi * dq['n']:.1f}), non c'e'")
print(f" traccia che lo faccia.")
# -- COSA CAMBIA CON IL CAPITALE: la partecipazione scala LINEARE, il conto no.
print(f"\n SCALA — l'unica parte di questa misura che ha una data di scadenza:")
eq = 636.0 # equity reale osservata (data/live/equity_seen.json)
print(f" la partecipazione scala lineare col capitale (equity oggi ~${eq:.0f}).")
print(f" {'capitale':>10} {'partecip. mediana':>19} {'p90':>9} {'max osservato':>15}")
for cap in (600, 5_000, 20_000, 100_000, 272_000):
k = cap / eq
mx = pr.max() * k * 100
print(f" {'$' + format(cap, ','):>10} {pr.median() * k * 100:>18.2f}% "
f"{pr.quantile(.9) * k * 100:>8.2f}% "
+ (f"{mx:>14.1f}%" if mx <= 100 else f"{'>tutta la barra':>15}"))
print(f" => a $600 l'impatto e' strutturalmente impossibile e questa misura lo conferma.")
print(f" Ma il caso peggiore osservato e' gia' {pr.max() * 100:.0f}% di una barra 5m: a $5k")
print(f" diventa {pr.max() * 5000 / eq * 100:.0f}%, a $20k {pr.max() * 20000 / eq * 100:.0f}%. "
f"**La misura di oggi NON si estrapola**")
print(f" al capitale del piano: va RIFATTA a ogni salto di taglia. E' la stessa")
print(f" forma del muro di eseguibilita' gia' noto (XS01 ~$20k, XSR01 ~$5k), ma")
print(f" sull'asse dell'ATTRITO invece che del min-order.")
# ---------------------------------------------------------------- 6. conversione
print("\n[6] CONVERSIONE IN SHARPE/CAGR — curva PUBBLICATA, non ricalcolata\n" + "-" * 96)
print(f" r0726_fee_sensitivity: {DSHARPE_PER_BPS_SIDE:+.3f} Sharpe/bps-lato · "
f"{DCAGR_PER_BPS_SIDE:+.2f} pp CAGR/bps-lato (SKH01 ~4x piu' sensibile di TP01)")
b5 = ci_mean(R["slip_B5"])
qs_mean = (dq["mean"] - 0.5) * rng_med
qs_lo, qs_hi = (dq["lo"] - 0.5) * rng_med, (dq["hi"] - 0.5) * rng_med
for nm, m, lo, hi in (("REF-B5 (bps, senza potenza)", b5["mean"], b5["lo"], b5["hi"]),
("q normalizzata (con potenza)", qs_mean, qs_lo, qs_hi)):
print(f" {nm:<30} slip {m:+6.2f} bps -> dSharpe {DSHARPE_PER_BPS_SIDE * m:+.4f} "
f"· dCAGR {DCAGR_PER_BPS_SIDE * m:+.3f} pp "
f"[IC95 dSharpe {DSHARPE_PER_BPS_SIDE * hi:+.4f}..{DSHARPE_PER_BPS_SIDE * lo:+.4f}]")
print(f"\n Margine gia' pagato dal modello: {MARGIN_BPS_SIDE:.1f} bps/lato "
f"(backtest 10 bps RT - fee vera 7 bps RT).")
print(f" Slippage con potenza (q) dentro il margine? "
f"{'SI' if qs_hi <= MARGIN_BPS_SIDE else 'NON DIMOSTRATO'} "
f"(estremo alto IC95 {qs_hi:+.2f} vs {MARGIN_BPS_SIDE:.1f})")
# ---------------------------------------------------------------- 7. taglia e ora
print("\n[7] DIPENDENZA DA TAGLIA E ORA — il campione lo permette?\n" + "-" * 96)
from scipy import stats
sub = R.dropna(subset=["q"])
rho, p = stats.spearmanr(sub["notional"], sub["q"])
print(f" taglia ESEGUITA ${R['notional'].min():.0f}-${R['notional'].max():.0f} "
f"(mediana ${R['notional'].median():.0f}). NB: e' l'INCREMENTO mandato al venue, non il")
print(f" target di posizione (che arriva a $189): il libro riconcilia, non ri-apre.")
print(f" Spearman(notional, q) = {rho:+.3f} (p={p:.2f})")
big = sub[sub["notional"] >= sub["notional"].median()]["q"]
small = sub[sub["notional"] < sub["notional"].median()]["q"]
print(f" meta' grande n={len(big)} q={big.mean():.3f} · meta' piccola n={len(small)} "
f"q={small.mean():.3f} · differenza {big.mean() - small.mean():+.3f} "
f"(MDE su un confronto a meta' campione ~{2.80 * (1 / np.sqrt(12)) * np.sqrt(2 / (n / 2)):.3f})")
hrs = sorted(int(h) for h in R["fill_ts"].dt.hour.unique())
print(f" ore del giorno coperte: {hrs} ({len(hrs)} ore distinte su 24, "
f"{R['fill_ts'].dt.hour.value_counts().max()} fill nell'ora piu' popolata)")
print(f" => con n={n} e {len(hrs)} ore distinte, l'effetto ora NON e' stimabile: "
f"lo dichiaro non misurato, non 'assente'.")
# ---------------------------------------------------------------- 8. netting
print("\n[8] NETTING TP01/SKH01 — cosa NON e' misurabile da questi dati\n" + "-" * 96)
print(f" Il libro netta TP01 e SKH01 su UNO strumento: ogni fill e' la RISULTANTE, mai")
print(f" il trade di uno sleeve. Quindi lo slippage misurato e' quello del LIBRO, e non si")
print(f" puo' attribuire a TP01 o a SKH01 — proprio mentre SKH01 e' ~4x piu' sensibile.")
print(f" Il netting RIDUCE il turnover eseguito: {n} ordini in "
f"{(fills['fill_ts'].max() - fills['fill_ts'].min()).days} giorni, "
f"nozionale totale ${R['notional'].sum():,.0f}.")
print("\n" + "=" * 96)
out = SCRATCH / "r0822_slip_fills.csv"
SCRATCH.mkdir(parents=True, exist_ok=True)
R.to_csv(out, index=False)
print(f" dettaglio per fill: {out}")
print("=" * 96)
if __name__ == "__main__":
main()
+941
View File
@@ -0,0 +1,941 @@
"""TERM-STRUCTURE della vol implicita BTC/ETH — pendenza, carry, gate di rischio (ondata 2026-08-22).
DOMANDA. Tre, dichiarate prima di misurare:
Q1 la PENDENZA (backwardation = front > back) PRECEDE i ritorni, o e' contemporanea?
Q2 il CARRY di vol (roll-down lungo la curva) e' INCASSABILE al bid-ask reale?
Q3 vale come GATE DI RISCHIO sopra TP01/SKH01?
IPOTESI A PRIORI (⚠️ da dove vengono: NON da una busta sigillata. Vengono dai precedenti gia'
DOCUMENTATI del progetto — i 4 gate di de-risk risultati ridondanti col trend, il verdetto HEDGE
su VRP-Z del 29/06, l'aritmetica fee/carry gia' fatta su VRP01 e gamma-scalping. Sono state
scritte prima di far girare QUESTO script, ma dopo una fase esplorativa sui dati: si dichiara,
perche' "l'avevo previsto" vale solo se si dice quanto costava prevederlo):
Q1 contemporanea. Il front reagisce allo spot: un tuffo alza la vol a 7g piu' di quella a 30g.
Se e' cosi', la pendenza e' un TERMOMETRO, non un segnale.
Q2 no. Il roll-down fra 7g e 30g vale frazioni di punto-vol al giorno; il bid-ask ATM di
Deribit vale punti-vol INTERI. L'aritmetica dovrebbe chiudere la questione senza backtest.
Q3 ridondante col trend, come i 4 precedenti (DVOL-spike, macro, funding, breadth): un gate di
de-risk lavora nei giorni in cui TP01 e' gia' flat.
IL PRECEDENTE SCOMODO (03/07, `r0703_vrpimp_*`): un gate di term-structure VIX/VXV su SPX valeva
+0.90 di Sharpe (DSR 0.992) ed era un **confound di modello al 100%** — la variabile del gate
coincideva con l'errore di prezzatura BS-flat vs term-structure. Da li' la regola: *riprezzare
term-structure-consistent prima di credere a un gate vol su strutture BS-flat*.
QUI QUEL CONFOUND NON PUO' ESISTERE: non si prezza nessuna opzione. Il sottostante e' il perp
BTC/ETH, i ritorni sono quelli del feed certificato, e la term structure entra solo come
VARIABILE OSSERVATA. Quindi il 03/07 non e' una scusa per non guardare — ed e' esattamente
percio' che serve cercare il confound *di questo* filone, che e' un altro:
⚠️ IL CONFOUND DI QUESTO FILONE: iv(7g) e' in larga parte una funzione della vol REALIZZATA
recente. Se e' cosi', `slope = iv30 - iv7` non e' un'informazione nuova ma un
RICONFEZIONAMENTO di RV — cioe' la stessa variabile di `dvol_directional.py` (VRP-Z), gia'
giudicata **HEDGE, earns_slot=False** su 5 anni il 2026-06-29. Misurato in SEZIONE 2.
IL DATO, e il muro che ci sta dentro. La term structure richiede >=2 scadenze nello STESSO
istante. `bite_archive` prima del **2026-06-09** collezionava **UNA scadenza per giro**: la
finestra utilizzabile NON e' "3,7 mesi di catena", e' **dal 2026-06-09** (~74 giorni).
Con 74 giorni **non esiste un hold-out** e SE(Sharpe) ~ sqrt(365/74) ~ 2.2: qualunque Sharpe
misurato qui e' a una deviazione standard da zero. Verdetto massimo: LEAD con gate e data.
Si conta cio' che e' QUOTATO (bid>0, ask>0, iv presente), non le righe: il guasto 29-30/07 ha
fino al 51% di quote vuote in un giorno.
METODO. ATM IV per scadenza = interpolazione della IV sul DELTA a 0.5 (call) / -0.5 (put), mai
estrapolata. Il delta referenzia il FORWARD, quindi la curva non eredita lo skew via la base
(prendere "lo strike piu' vicino allo spot" lo farebbe: a 90g il forward e' lontano dallo spot).
Interpolazione ai tenori fissi in VARIANZA TOTALE (w = iv^2 * T lineare in T), stile VIX.
nice -n 19 timeout 900 uv run python scripts/research/r0822_term_structure.py
"""
from __future__ import annotations
import os
import sys
import tempfile
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" / "alt"))
import altlib as A # noqa: E402
RAW = ROOT / "data" / "raw"
STORE = RAW / "cb_chain"
CACHE = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) / "r0822_ts_cache"
ASSETS = ("BTC", "ETH")
# Muro del dato: prima di questa data l'archivio ha UNA scadenza per giro -> nessuna curva.
TS_START = pd.Timestamp("2026-06-09", tz="UTC")
TENORS = (7, 14, 30, 60, 90)
FRONT, BACK = 7, 30 # la pendenza canonica di questo studio
DTE_MIN = 0.5 # sotto mezza giornata l'ATM IV e' rumore di microstruttura
# Vincoli di venue DICHIARATI (misurati il 2026-07-30 su `get_instrument`, non ri-letti qui:
# questo script non tocca la rete). Servono alla sezione di eseguibilita'.
MIN_LOT = {"BTC": 0.1, "ETH": 1.0} # contratti minimi opzioni Deribit
OPT_FEE_UNDERLYING = 0.0003 # 0,03% del sottostante per gamba
OPT_FEE_CAP_PREMIUM = 0.125 # cap: 12,5% del premio della singola opzione
CAPITALE = 635.0 # il conto vero
# Conteggio dei trial, AL RIALZO (sezione 6). Ogni voce e' una configurazione VALUTATA.
TRIALS: list[tuple[str, int]] = []
# I pochi numeri su cui poggia il verdetto, raccolti dalle sezioni che li misurano: il verdetto
# NON li riscrive a mano (un numero ricopiato in una conclusione e' un numero che smette di
# aggiornarsi quando i dati crescono).
FACTS: dict = {}
def hr(t: str = "") -> None:
print("\n" + "=" * 100)
if t:
print(t)
print("=" * 100)
def sub(t: str) -> None:
print(f"\n--- {t} " + "-" * max(0, 96 - len(t)))
# ===========================================================================================
# SEZIONE 0 — IL DATO
# ===========================================================================================
def load_quoted_chain() -> pd.DataFrame:
"""Catena filtrata IN LETTURA (colonne + finestra), tenendo solo cio' che e' QUOTATO.
"Quotato" = bid>0 AND ask>0 AND iv presente AND delta presente. Una riga con bid/ask NULL
esiste (il collettore la persiste) ma non e' un prezzo: contarla come dato e' l'errore che
il progetto ha gia' fatto tre volte (paper_dvolspread, fresh_5m, guasto 29/07).
"""
cols = ["asset", "option_type", "strike", "iv", "delta", "bid", "ask",
"vega", "ts", "exp", "quote_status"]
parts = []
arch = STORE / "bite_archive.parquet"
if arch.exists():
d = pd.read_parquet(arch, columns=cols)
parts.append(d[d["ts"] >= TS_START])
del d
for p in sorted(STORE.glob("2026-*.parquet")):
parts.append(pd.read_parquet(p, columns=cols))
if not parts:
raise FileNotFoundError(f"{STORE}: nessuna catena")
df = pd.concat(parts, ignore_index=True)
del parts
df = df.drop_duplicates(subset=["ts", "asset", "strike", "option_type", "exp"], keep="last")
# ⚠️ le quote di bite sono datetime64[us], il feed certificato e' [ns]: un join fra
# DatetimeIndex di unita' diverse degrada a Index generico e rompe ogni resample a valle.
df["hr"] = df["ts"].dt.floor("h").dt.as_unit("ns")
df["exp"] = df["exp"].dt.as_unit("ns")
df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0
df["quoted"] = (df["bid"] > 0) & (df["ask"] > 0) & df["iv"].notna() & df["delta"].notna()
return df
def sezione0(chain: pd.DataFrame) -> None:
hr("SEZIONE 0 — IL DATO: quanto ce n'e' davvero, e da quando")
print(f" finestra letta : {chain.ts.min()} -> {chain.ts.max()}")
print(f" righe : {len(chain):,}")
print(f" righe QUOTATE : {int(chain.quoted.sum()):,} ({chain.quoted.mean():.1%})")
print("\n ⚠️ IL MURO: la term structure richiede >=2 scadenze nello STESSO istante.")
arch = STORE / "bite_archive.parquet"
if arch.exists():
pre = pd.read_parquet(arch, columns=["asset", "ts", "exp"])
pre["hr"] = pre["ts"].dt.floor("h")
n_pre = pre[pre.ts < TS_START].groupby(["asset", "hr"])["exp"].nunique()
n_post = pre[pre.ts >= TS_START].groupby(["asset", "hr"])["exp"].nunique()
print(f" archivio PRIMA del {TS_START.date()}: scadenze/ora mediana = "
f"{n_pre.median():.0f} (max {n_pre.max():.0f}) su {len(n_pre)} ore-asset")
print(f" archivio DOPO : mediana = {n_post.median():.0f} "
f"(max {n_post.max():.0f}) su {len(n_post)} ore-asset")
print(" => la catena copre dal 2026-05-01, ma la CURVA esiste solo dal 2026-06-09.")
print(" Non sono 3,7 mesi: sono ~74 giorni. Nessun hold-out e' costruibile.")
del pre
q = chain.groupby([chain.hr.dt.date, "asset"])["quoted"].mean().unstack()
bad = q[(q < 0.70).any(axis=1)]
print(f"\n giorni con quote quotate <70% su almeno un asset: {len(bad)}")
if len(bad):
print(bad.round(3).to_string().replace("\n", "\n "))
print(" (guasto noto 29-30/07: rate-limit per-IP saturato dal collettore di bite.)")
print(f"\n minuto dello snapshot (mediana): archivio bite = "
f"{chain[chain.ts < pd.Timestamp('2026-07-30', tz='UTC')].ts.dt.minute.median():.0f}', "
f"raccolta propria = {chain[chain.ts >= pd.Timestamp('2026-07-31', tz='UTC')].ts.dt.minute.median():.0f}'")
print(" -> lo snapshot dell'ora t cade DENTRO la barra oraria t. Ogni ritorno usato come")
print(" bersaglio parte dalla CHIUSURA della barra t (>=35 min dopo): buffer causale.")
# ===========================================================================================
# SEZIONE 1 — RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE
# ===========================================================================================
def _interp_at(gid: np.ndarray, x: np.ndarray, y: np.ndarray, ngrp: int,
target: float) -> np.ndarray:
"""Interpolazione lineare di y(x) a `target`, per ogni gruppo, VETTORIALE e senza
estrapolazione (NaN se il gruppo non racchiude il target).
`gid` dev'essere ordinato crescente e (gid, x) lessicograficamente ordinati.
Equivalente a np.interp gruppo-per-gruppo: qui serve solo perche' i gruppi sono ~30.000
e un ciclo python li paga a caro prezzo su una VPS a 2 core condivisa.
"""
n_tot = np.bincount(gid, minlength=ngrp)
start = np.concatenate([[0], np.cumsum(n_tot)[:-1]])
n_below = np.bincount(gid[x <= target], minlength=ngrp)
ok = (n_below > 0) & (n_below < n_tot)
out = np.full(ngrp, np.nan)
if not ok.any():
return out
lo = (start + n_below - 1)[ok]
hi = lo + 1
x0, x1, y0, y1 = x[lo], x[hi], y[lo], y[hi]
w = np.where(x1 != x0, (target - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0)
out[ok] = y0 + w * (y1 - y0)
return out
def build_atm_by_expiry(chain: pd.DataFrame) -> pd.DataFrame:
"""ATM IV per (asset, ora, scadenza) = IV interpolata sul DELTA a 0.5 (call) / -0.5 (put).
Perche' il delta e non "lo strike piu' vicino allo spot": il delta referenzia il FORWARD.
A 90 giorni il forward sta lontano dallo spot, quindi lo strike ATM-spot e' un'opzione OTM
e la sua IV porta dentro lo SKEW — che e' un altro filone e falserebbe la pendenza in modo
sistematico e crescente col tenore. Nessuna estrapolazione: se il delta 0.5 non e' racchiuso
dai quotati, quella scadenza a quell'ora non ha ATM e viene scartata.
"""
f = CACHE / "atm_by_expiry.parquet"
if f.exists():
return pd.read_parquet(f)
q = chain[chain.quoted & (chain.dte > DTE_MIN)].copy()
keys = ["asset", "hr", "exp"]
q["gid"] = q.groupby(keys, sort=True).ngroup()
ngrp = int(q["gid"].max()) + 1
ivs = []
for typ, tgt in (("C", 0.5), ("P", -0.5)):
s = q[q.option_type == typ].sort_values(["gid", "delta"])
v = np.full(ngrp, np.nan)
if len(s):
g2 = s["gid"].to_numpy()
uniq, inv = np.unique(g2, return_inverse=True)
v[uniq] = _interp_at(inv, s["delta"].to_numpy(), s["iv"].to_numpy(), len(uniq), tgt)
ivs.append(v)
M = np.vstack(ivs)
with np.errstate(invalid="ignore"):
cnt = np.sum(np.isfinite(M), axis=0)
iv = np.where(cnt > 0, np.nansum(np.where(np.isfinite(M), M, 0.0), axis=0) / np.maximum(cnt, 1), np.nan)
out = (q.groupby(keys, sort=True)["dte"].median().reset_index())
out["iv"] = iv
out = out.dropna(subset=["iv"]).reset_index(drop=True)
CACHE.mkdir(parents=True, exist_ok=True)
out.to_parquet(f)
return out
def _curve_row(g: pd.DataFrame) -> pd.Series:
g = g.sort_values("dte")
t = g["dte"].to_numpy()
w = (g["iv"].to_numpy() / 100.0) ** 2 * t # varianza totale
o = {}
for T in TENORS:
o[f"iv_{T}"] = (np.sqrt(np.interp(T, t, w) / T) * 100.0
if len(t) >= 2 and t.min() <= T <= t.max() else np.nan)
o["n_exp"] = len(t)
return pd.Series(o)
def build_curve(atm: pd.DataFrame) -> pd.DataFrame:
f = CACHE / "curve.parquet"
if f.exists():
return pd.read_parquet(f)
c = atm.groupby(["asset", "hr"]).apply(_curve_row, include_groups=False).reset_index()
CACHE.mkdir(parents=True, exist_ok=True)
c.to_parquet(f)
return c
def sezione1(curve: pd.DataFrame) -> dict:
hr("SEZIONE 1 — RICOSTRUZIONE DELLA CURVA E SUA CERTIFICAZIONE")
print(" ATM = IV interpolata sul delta 0.5/-0.5 (mai estrapolata); tenori fissi in varianza totale.")
cov = curve[[f"iv_{T}" for T in TENORS]].notna().mean()
print("\n copertura per tenore (frazione delle ore-asset ricostruibili):")
for T in TENORS:
print(f" iv_{T:<3d} {cov[f'iv_{T}']:.1%}")
print(f" -> iv_90 e' sotto il 10%: SCARTATO dallo studio. La pendenza canonica e' "
f"iv_{BACK} - iv_{FRONT}.")
sub("controllo 1 — contro il logger indipendente `vol_term_*.parquet` (altro codice, altra fonte)")
print(" (`log_vol_termstructure.py`: mark_iv da book_summary, ATM = strike piu' vicino allo")
print(" spot, interpolazione diversa. Se le due strade coincidono, la mia non e' un artefatto.)")
agree = {}
for a in ASSETS:
p = RAW / f"vol_term_{a.lower()}.parquet"
if not p.exists():
print(f" {a}: {p.name} assente — controllo NON GIRATO")
continue
vt = pd.read_parquet(p).set_index("date")
mine = curve[curve.asset == a].set_index("hr")
j = vt.join(mine[[f"iv_{T}" for T in TENORS]], how="inner")
for T in (7, 30, 60):
x, y = j[f"iv_{T}d"], j[f"iv_{T}"]
m = x.notna() & y.notna()
if m.sum() >= 10:
print(f" {a} iv_{T:<3d} n={m.sum():3d} corr={x[m].corr(y[m]):+.4f} "
f"scarto medio={(y[m] - x[m]).mean():+.3f} pt-vol")
agree[(a, T)] = float(x[m].corr(y[m]))
sub("controllo 2 — contro il DVOL (indice Deribit a 30g, storia 2021+)")
for a in ASSETS:
dv = pd.read_parquet(RAW / f"dvol_{a.lower()}.parquet")
s = pd.Series(dv["close"].astype(float).values,
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True)).sort_index()
mine = curve[curve.asset == a].set_index("hr")["iv_30"].dropna()
j = pd.DataFrame({"mine": mine})
j["dvol"] = s.reindex(j.index, method="ffill")
j = j.dropna()
print(f" {a}: n={len(j)} corr={j.mine.corr(j.dvol):+.4f} "
f"bias={(j.mine - j.dvol).mean():+.3f} pt-vol sd={(j.mine - j.dvol).std():.3f}")
print(" Il bias NEGATIVO e' atteso e conferma la ricostruzione: il DVOL e' un indice tipo")
print(" variance-swap (integra tutto lo smile) e sta STRUTTURALMENTE sopra l'ATM per lo skew.")
print(" Un bias ~0 sarebbe stato il segnale d'allarme, non questo.")
return agree
# ===========================================================================================
# SEZIONE 2 — Q1: LEAD-LAG. La pendenza precede o segue?
# ===========================================================================================
def slope_hourly(curve: pd.DataFrame, a: str) -> pd.Series:
s = curve[curve.asset == a].copy()
s["hr"] = pd.to_datetime(s["hr"], utc=True).dt.as_unit("ns")
s = s.set_index("hr")
return (s[f"iv_{BACK}"] - s[f"iv_{FRONT}"]).dropna().sort_index()
def px_hourly(a: str) -> pd.Series:
d = A.get(a, "1h")
return pd.Series(d["close"].astype(float).values,
index=pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)))
def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 400,
seed: int = 20260822) -> float:
"""Percentile della correlazione osservata contro il null a BLOCCHI (y ricampionata da
blocchi non allineati a x): conserva l'autocorrelazione e rompe solo l'accoppiamento."""
rng = np.random.default_rng(seed)
m = len(x)
if m <= L + 5:
return float("nan")
nb = max(1, m // L)
out = np.empty(n)
for i in range(n):
ii = np.concatenate([np.arange(s, s + L) for s in rng.integers(0, m - L, nb)])
jj = np.concatenate([np.arange(s, s + L) for s in rng.integers(0, m - L, nb)])
out[i] = np.corrcoef(x[ii], y[jj])[0, 1]
return float((out < stat).mean())
def sezione2(curve: pd.DataFrame) -> dict:
hr("SEZIONE 2 — Q1: la pendenza PRECEDE i ritorni o li SEGUE?")
res = {}
for a in ASSETS:
sl = slope_hourly(curve, a)
px = px_hourly(a)
lp = np.log(px)
j = pd.DataFrame({"slope": sl}).join(px.rename("px"), how="inner").dropna()
idx = j.index
r1 = lp.diff()
sub(f"{a} — n ore = {len(j)} ({idx.min()} -> {idx.max()})")
FACTS[f"back_sev_{a}"] = float((j.slope < -5).mean())
FACTS[f"back_{a}"] = float((j.slope < 0).mean())
print(f" pendenza iv_{BACK}-iv_{FRONT}: media {j.slope.mean():+.2f} pt-vol, "
f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore, "
f"SEVERA (< -5 pt-vol) nel {100*(j.slope<-5).mean():.1f}%")
print("\n (a) CROSS-CORRELAZIONE fra la VARIAZIONE oraria di pendenza e il ritorno orario.")
print(" lag<0 = ritorno PRIMA della variazione (il prezzo guida) | lag>0 = DOPO (la")
print(" pendenza guiderebbe). La barra t va da t a t+1h e lo snapshot cade dentro:")
print(" il lag -1 e' quindi in gran parte SOVRAPPOSTO, non un vero anticipo.")
ds = j.slope.diff()
row = []
for k in (-3, -2, -1, 0, 1, 2, 3, 6, 12, 24):
row.append((k, float(ds.corr(r1.shift(-k).reindex(idx)))))
print(" " + " ".join(f"{k:+d}h:{v:+.3f}" for k, v in row))
best = max(row, key=lambda t: abs(t[1]))
print(f" picco |corr| al lag {best[0]:+d}h ({best[1]:+.3f})")
print("\n (b) LIVELLO della pendenza vs ritorni STRETTAMENTE FUTURI (ingresso alla")
print(" chiusura della barra t) e vs ritorni PASSATI.")
for h in (1, 3, 6, 24, 72, 168):
fwd = (lp.shift(-h) - lp).reindex(idx)
pst = (lp - lp.shift(h)).reindex(idx)
m = j.slope.notna() & fwd.notna() & pst.notna()
print(f" h={h:4d}h corr(pendenza, FUTURO) {j.slope[m].corr(fwd[m]):+.3f} "
f"corr(pendenza, PASSATO) {j.slope[m].corr(pst[m]):+.3f}")
TRIALS.append((f"Q1 {a}: 6 orizzonti x (futuro,passato) x (livello,variazione)", 24))
print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?")
rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx)
# RV su (t, t+24h]: rolling(24) etichettata a t+24 e riportata indietro a t. (Una prima
# stesura aveva uno shift di troppo e misurava la finestra (+24h, +47h): corretto in
# sessione confrontando con una seconda implementazione a serie rovesciata.)
rvf = (r1.rolling(24).std().shift(-24) * np.sqrt(24 * 365) * 100).reindex(idx)
cf = curve[curve.asset == a].copy()
cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns")
ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx)
c_pp = float(j.slope.corr(rvp))
c_pf = float(j.slope.corr(rvf))
print(f" corr(pendenza, RV 24h PASSATA) {c_pp:+.3f} <-- quanto e' termometro")
print(f" corr(pendenza, RV 24h FUTURA) {c_pf:+.3f} <-- quanto e' previsione")
print(f" corr(iv_{FRONT}, RV 24h passata) {float(ivf.corr(rvp)):+.3f} "
f"corr(iv_{FRONT}, RV futura) {float(ivf.corr(rvf)):+.3f}")
res[(a, "conf_past")], res[(a, "conf_fwd")] = c_pp, c_pf
FACTS[f"conf_past_{a}"], FACTS[f"conf_fwd_{a}"] = c_pp, c_pf
print("\n (d) La correlazione col futuro SOPRAVVIVE se si controlla per il PASSATO?")
print(" (regressione del ritorno futuro su pendenza + ritorno passato + RV passata;")
print(" se il coefficiente della pendenza evapora, il segnale era 'e' appena sceso')")
for h in (24, 72, 120):
fwd = (lp.shift(-h) - lp).reindex(idx)
pst = (lp - lp.shift(h)).reindex(idx)
m = j.slope.notna() & fwd.notna() & pst.notna() & rvp.notna()
X = np.column_stack([np.ones(m.sum()), j.slope[m], pst[m], rvp[m]])
y = fwd[m].to_numpy()
b, *_ = np.linalg.lstsq(X, y, rcond=None)
# errori standard Newey-West (lag = h, finestre sovrapposte)
e = y - X @ b
XtXi = np.linalg.pinv(X.T @ X)
S = (X * e[:, None]).T @ (X * e[:, None])
for L in range(1, h + 1):
w = 1.0 - L / (h + 1.0)
G = (X[L:] * e[L:, None]).T @ (X[:-L] * e[:-L, None])
S += w * (G + G.T)
se = np.sqrt(np.diag(XtXi @ S @ XtXi))
simple = float(j.slope[m].corr(fwd[m]))
print(f" h={h:4d}h corr semplice {simple:+.3f} | beta_pendenza={b[1]:+.5f} "
f"t(NW)={b[1]/se[1]:+.2f} | beta_ret_passato t={b[2]/se[2]:+.2f}")
TRIALS.append((f"Q1 {a}: 3 regressioni controllate", 3))
print("\n (e) Test giornaliero con null a BLOCCHI (la finestra e' di 74 giorni: la")
print(" correlazione va confrontata con la sua dispersione, non con zero).")
sld = j.slope.resample("1D").last().dropna()
pxd = px.resample("1D").last()
lpd = np.log(pxd)
for h in (1, 3, 5):
fwd = (lpd.shift(-h) - lpd).reindex(sld.index)
m = sld.notna() & fwd.notna()
x, y = sld[m].to_numpy(), fwd[m].to_numpy()
c = float(np.corrcoef(x, y)[0, 1])
p = _block_pctl(x, y, c, L=max(3 * h, 7))
c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan
c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan
if h == 5:
FACTS[f"c5_{a}"], FACTS[f"c5_no7_{a}"] = c, c7
print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}"
f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}")
print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7")
print(" giorni su 74 sposta la correlazione di piu' di quanto la correlazione")
print(" valga, il campione non sta misurando, sta oscillando.")
TRIALS.append((f"Q1 {a}: 3 orizzonti giornalieri x 3 sottocampioni", 9))
return res
# ===========================================================================================
# SEZIONE 3 — Q2: il carry di vol e' incassabile?
# ===========================================================================================
def build_atm_cost(chain: pd.DataFrame) -> pd.DataFrame:
"""Costo REALE di attraversare lo spread su un'opzione ATM, in PUNTI DI VOL.
(ask-bid) e' quotato nel sottostante -> x spot = USD; / vega = punti di vol, che e' l'unita'
in cui si misura il roll-down. Cosi' costo e carry sono confrontabili senza modello.
"""
f = CACHE / "atm_cost.parquet"
if f.exists():
return pd.read_parquet(f)
q = chain[chain.quoted & (chain.dte > DTE_MIN) & (chain.option_type == "C") & (chain.vega > 0)].copy()
q["ad"] = (q["delta"] - 0.5).abs()
atm = q.sort_values("ad").groupby(["asset", "hr", "exp"], as_index=False).first()
atm = atm[atm.ad < 0.08]
for a in ASSETS:
p = px_hourly(a)
m = atm.asset == a
atm.loc[m, "spot"] = p.reindex(atm.loc[m, "hr"]).ffill().to_numpy()
atm = atm.dropna(subset=["spot"])
atm["spr_usd"] = (atm.ask - atm.bid) * atm.spot
atm["spr_vol"] = atm.spr_usd / atm.vega
atm["prem_usd"] = (atm.bid + atm.ask) / 2.0 * atm.spot
CACHE.mkdir(parents=True, exist_ok=True)
atm.to_parquet(f)
return atm
def _roll_table(r: pd.DataFrame, H: int) -> pd.DataFrame:
"""Roll IMPLICITO vs REALIZZATO su orizzonte H ore, appaiato per (scadenza, ISTANTE D'INGRESSO).
Appaiato sull'INGRESSO e non sull'uscita: e' l'uscita che le due letture spostano, e un
inner-join sulla data d'uscita terrebbe solo i casi in cui non e' successo niente (trappola
incontrata 3 volte nel progetto: venue-watch, GTAA, VRP profit-take).
impl = iv_curva(T-H) al tempo t - iv(T) al tempo t (se la curva non si muovesse)
real = iv(T-H) al tempo t+H - iv(T) al tempo t (cio' che e' successo davvero)
"""
r = r[["hr", "exp", "iv", "dte"]].dropna()
nxt = r[["hr", "exp", "iv"]].copy()
nxt["hr"] = nxt["hr"] - pd.Timedelta(hours=H)
j = r.merge(nxt.rename(columns={"iv": "iv_fut"}), on=["hr", "exp"], how="inner")
j["T2"] = j["dte"] - H / 24.0
j = j[j["T2"] > DTE_MIN]
if j.empty:
return pd.DataFrame(columns=["dte", "impl", "real"])
# curva dell'ora t, ordinata: chiave monotona globale gid*1000 + dte -> searchsorted unico
c = r.sort_values(["hr", "dte"]).copy()
c["gid"] = c.groupby("hr", sort=True).ngroup()
key = c["gid"].to_numpy() * 1000.0 + c["dte"].to_numpy()
w = (c["iv"].to_numpy() / 100.0) ** 2 * c["dte"].to_numpy() # varianza totale
gmap = dict(zip(c["hr"].to_numpy(), c["gid"].to_numpy()))
n_tot = np.bincount(c["gid"].to_numpy())
start = np.concatenate([[0], np.cumsum(n_tot)[:-1]])
gq = np.array([gmap[h] for h in j["hr"].to_numpy()])
pos = np.searchsorted(key, gq * 1000.0 + j["T2"].to_numpy(), side="left")
lo, hi = pos - 1, pos
ok = (lo >= start[gq]) & (hi < start[gq] + n_tot[gq])
ivT2 = np.full(len(j), np.nan)
if ok.any():
l, h2 = lo[ok], hi[ok]
x0 = key[l] - gq[ok] * 1000.0
x1 = key[h2] - gq[ok] * 1000.0
t2 = j["T2"].to_numpy()[ok]
ww = np.where(x1 != x0, (t2 - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0)
var = w[l] + ww * (w[h2] - w[l])
ivT2[ok] = np.sqrt(np.maximum(var, 0.0) / t2) * 100.0
j = j.assign(ivT2=ivT2).dropna(subset=["ivT2"])
return pd.DataFrame({"dte": j["dte"].to_numpy(),
"impl": j["ivT2"].to_numpy() - j["iv"].to_numpy(),
"real": j["iv_fut"].to_numpy() - j["iv"].to_numpy()})
def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> None:
hr("SEZIONE 3 — Q2: il CARRY di vol (roll-down) e' incassabile al bid-ask reale?")
print(" Definizione senza modello: un'opzione a tenore T, dopo dt, sta a T-dt. Se la curva non")
print(" si muove la sua IV diventa iv(T-dt). Chi e' CORTO guadagna iv(T)-iv(T-dt) punti di vol.")
print(" Il costo di entrare e uscire e' lo spread bid-ask, misurabile negli STESSI punti di vol.")
sub("(a) quanto vale il roll-down, e viene DAVVERO incassato?")
print(" Confronto appaiato per SCADENZA e per ISTANTE D'INGRESSO (mai allineato sull'uscita):")
print(" roll IMPLICITO = iv_curva(T-dt) - iv(T) al tempo t ; roll REALIZZATO = iv(T-dt) a t+dt - iv(T) a t")
roll_day = {}
for H in (24, 168):
for a in ASSETS:
d = _roll_table(atm_exp[atm_exp.asset == a], H)
d = d[(d.dte >= 4) & (d.dte <= 45)]
if len(d) < 50:
print(f" {a} dt={H}h: campione insufficiente ({len(d)})")
continue
b = np.polyfit(d.impl, d.real, 1)[0]
diff = d.real - d.impl
nblk = max(1, len(d) // max(1, H)) # osservazioni ~indipendenti
t = diff.mean() / diff.std() * np.sqrt(nblk) if diff.std() > 0 else 0.0
print(f" {a} dt={H:3d}h n={len(d):5d} roll implicito medio {d.impl.mean():+.4f} pt-vol "
f"realizzato {d.real.mean():+.4f} beta(real~impl)={b:+.2f} "
f"corr={d.impl.corr(d.real):+.3f} (real-impl) t={t:+.2f}")
if H == 24:
roll_day[a] = -float(d.impl.mean()) # guadagno giornaliero di CHI E' CORTO
print("\n Lettura: se il roll-down fosse incassato, beta(real~impl) sarebbe ~+1. E' NEGATIVO")
print(" (~-1 a una settimana): sul campione la FORMA della curva torna indietro piu' di")
print(" quanto la curva rotoli. Il segno del bias da errore-di-misura sarebbe POSITIVO")
print(" (iv(T) compare in entrambi con segno opposto), quindi non spiega questo.")
TRIALS.append(("Q2: 2 orizzonti x 2 asset (roll implicito vs realizzato)", 4))
sub("(b) il costo reale: bid-ask ATM in PUNTI DI VOL, per tenore")
cost = build_atm_cost(chain)
cost["bucket"] = pd.cut(cost.dte, [DTE_MIN, 3, 10, 20, 45, 95],
labels=["1-3g", "3-10g", "10-20g", "20-45g", "45-95g"])
tab = cost.groupby(["asset", "bucket"], observed=True).agg(
n=("spr_vol", "size"), spread_ptvol=("spr_vol", "median"),
spread_pct_premio=("spr_usd", lambda s: np.nan),
premio_usd=("prem_usd", "median"), vega_usd_per_ptvol=("vega", "median"))
tab["spread_pct_premio"] = (cost.assign(x=cost.spr_usd / cost.prem_usd * 100)
.groupby(["asset", "bucket"], observed=True)["x"].median())
print(tab.round(3).to_string().replace("\n", "\n "))
sub("(c) l'aritmetica che chiude la domanda")
med = cost.groupby(["asset", "bucket"], observed=True)["spr_vol"].median()
for a in ASSETS:
rd = roll_day.get(a, np.nan)
sp = float(med.loc[(a, "10-20g")])
print(f" {a}: roll-down incassabile da CORTO = {rd:+.4f} pt-vol/giorno | "
f"un round-trip ATM 10-20g costa {sp:.3f} pt-vol")
print(f" => servono {sp/rd:.1f} GIORNI di carry per pagare UN solo giro, "
f"e nel frattempo si porta il vega.")
FACTS[f"giorni_carry_{a}"] = sp / rd
TRIALS.append(("Q2: 5 secchi di tenore x 2 asset (costo)", 10))
sub("(d) e a $635 la domanda non e' nemmeno questa: il LOTTO MINIMO")
for a in ASSETS:
c = cost[(cost.asset == a) & (cost.bucket == "10-20g")]
if c.empty:
continue
prem = float(c.prem_usd.median()) * MIN_LOT[a]
vega = float(c.vega.median()) * MIN_LOT[a]
spot = float(c.spot.median())
fee = min(OPT_FEE_UNDERLYING * spot, OPT_FEE_CAP_PREMIUM * float(c.prem_usd.median())) * MIN_LOT[a]
rd = roll_day.get(a, np.nan)
carry_g = rd * vega
friz = float(c.spr_vol.median()) * vega + 4 * fee # 1 spread pieno + 4 gambe di fee
print(f" {a}: lotto minimo {MIN_LOT[a]} -> premio ATM 10-20g = ${prem:,.2f} "
f"({prem/CAPITALE:.0%} del conto), vega ${vega:.2f}/pt-vol, fee ${fee:.2f}/gamba")
print(f" carry lordo ${carry_g:+.3f}/giorno per struttura; attrito per giro "
f"${friz:.2f} => {friz/carry_g:.0f} giorni per andare in pari")
print("\n Un calendar spread e' DUE gambe. Cio' che la tabella mostra e' il PREMIO di UNA")
print(" gamba al lotto minimo: BTC 26% del conto, ETH 10%. Il premio non e' tutto il")
print(" fabbisogno — la gamba CORTA vuole margine, e la misura del 2026-07-30 su una")
print(" struttura BTC a rischio definito dava $6.210 di collaterale per lotto. Quindi:")
print(" BTC resta fuori per il margine, ETH entra ma una sola struttura impegna il")
print(" 10-25% del conto per un carry LORDO di centesimi al giorno.")
print(" Il bersaglio dichiarato e' 50 EUR/giorno: qui siamo 2-3 ordini di grandezza sotto,")
print(" e il conto non regge la SECONDA struttura che servirebbe per diversificare.")
TRIALS.append(("Q2: 2 asset x eseguibilita' al lotto minimo", 2))
# ===========================================================================================
# SEZIONE 4 — Q3: gate di rischio sopra TP01. RIDONDANZA PRIMA DELL'UPLIFT.
# ===========================================================================================
def tp01_positions() -> dict:
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio
tp = TrendPortfolio(**CANONICAL)
out = {}
for a in ASSETS:
df = A.get(a, "1d")
out[a] = pd.Series(tp.target_series(df),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
return out
def _anchored(sl_h: pd.Series, px_h: pd.Series, off: int):
"""Serie giornaliere (pendenza, ritorno) ancorate all'ora `off`."""
m = sl_h.index.hour == off
s = sl_h[m]
p = px_h.reindex(s.index).ffill()
r = p.pct_change()
return s, r
def sezione4(curve: pd.DataFrame) -> None:
hr("SEZIONE 4 — Q3: la pendenza come GATE DI RISCHIO sopra TP01. RIDONDANZA PRIMA DELL'UPLIFT")
print(" Regola del progetto: 4 gate di de-risk su 4 (DVOL-spike, macro, funding, breadth) sono")
print(" risultati RIDONDANTI col trend. Percio' la ridondanza si misura PRIMA di rivendicare")
print(" qualunque uplift: quanti dei giorni che il gate spegne sono giorni in cui TP01 e' gia' flat?")
pos = tp01_positions()
sub("(a) il fatto che decide da solo la sezione")
W = pd.concat(pos, axis=1).loc[TS_START:]
flat = (W.abs() < 0.05).mean()
FACTS["tp01_flat"] = float(flat.mean())
FACTS["n_giorni"] = int(len(W))
print(f" Nella finestra {TS_START.date()} -> oggi ({len(W)} giorni), TP01 e' FLAT nel "
f"{flat['BTC']:.1%} (BTC) / {flat['ETH']:.1%} (ETH) dei giorni.")
print(f" Esposizione media: BTC {W['BTC'].mean():.3f}, ETH {W['ETH'].mean():.3f} "
f"(target TSMOM risk-off, coerente col libro live dichiarato flat).")
print(" ⚠️ Un gate di de-risk su un libro gia' flat il 93% del tempo NON E' MISURABILE su")
print(" questa finestra: non c'e' rischio da togliere. Questo non e' un difetto del")
print(" gate, e' un difetto del CAMPIONE — e va detto prima di qualunque numero.")
sub("(b) ridondanza, per quel che il campione consente")
for a in ASSETS:
sl = slope_hourly(curve, a).resample("1D").last().dropna()
sl.index = pd.DatetimeIndex(sl.index).as_unit("ns")
j = pd.concat({"slope": sl, "tp": pos[a]}, axis=1, join="inner").dropna()
for qq in (0.10, 0.25, 0.40):
th = j.slope.quantile(qq)
off = j.slope < th
already = float((j.tp[off].abs() < 0.05).mean()) if off.any() else np.nan
print(f" {a} gate OFF sotto p{int(qq*100)} ({th:+.2f} pt-vol): spegne il "
f"{off.mean():.1%} dei giorni; di questi TP01 era GIA' flat nel {already:.1%} "
f"| corr(pendenza, esposizione TP01) = {j.slope.corr(j.tp):+.3f}")
TRIALS.append((f"Q3 {a}: 3 soglie di gate", 3))
print("\n Il segno di corr(pendenza, esposizione) e' NEGATIVO: la backwardation arriva")
print(" quando TP01 e' gia' uscito. E' la firma esatta dei 4 precedenti.")
sub("(c) uplift sulla finestra, su TUTTE le 24 ancore (mai su una sola)")
print(" Candidato DIREZIONALE (non gate): contango -> long, backwardation -> flat; vol-target")
print(" 14g, cap 2x, fee 5 bps/lato. Soglia a ZERO = a priori, nessun parametro tarato.")
for a in ASSETS:
sl_h, px_h = slope_hourly(curve, a), px_hourly(a)
def cand(off, s=sl_h, p=px_h):
s_d, r_d = _anchored(s, p, off)
d = pd.DataFrame({"slope": s_d, "r": r_d}).dropna()
if len(d) < 30:
return pd.Series(dtype=float)
vol = d["r"].rolling(14).std() * np.sqrt(365)
dirn = (d["slope"] > 0).astype(float)
tgt = np.clip(dirn * (0.20 / vol.replace(0, np.nan)), 0, 2.0).fillna(0.0)
hold = tgt.shift(1).fillna(0.0)
return (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna()
def base(off, p=px_h, s=sl_h):
s_d, r_d = _anchored(s, p, off)
d = pd.DataFrame({"r": r_d}).dropna()
return d["r"]
band = A.anchor_luck_band(cand, list(range(24)), canonical=0)
print(f" {a}: Sharpe candidato — canonico(00:00) {band['canonical']:+.2f} "
f"(pctl {band['canonical_pctl']:.2f}) | MEDIANA ONESTA {band['median']:+.2f} | "
f"banda [{band['lo']:+.2f}, {band['hi']:+.2f}] | positivo in {band['frac_positive']:.0%} "
f"delle 24 ancore | gate_pass={band['gate_pass']}")
bh = A.anchor_luck_band(base, list(range(24)), canonical=0)
print(f" buy&hold sulla stessa finestra/ancore: mediana {bh['median']:+.2f} "
f"[{bh['lo']:+.2f}, {bh['hi']:+.2f}] <-- il vero termine di paragone in un toro")
dlt = A.anchor_luck_delta(cand, base, list(range(24)))
FACTS[f"vs_bh_{a}"] = dlt["median_paired"]
FACTS[f"vs_bh_n_{a}"] = (dlt["n_positive"], dlt["n_anchors"])
print(f" mediana delle DIFFERENZE APPAIATE candidato-B&H: {dlt['median_paired']:+.2f} "
f"(positiva in {dlt['n_positive']}/{dlt['n_anchors']} ancore)")
TRIALS.append((f"Q3 {a}: 24 ancore x 2 varianti", 48))
sub("(d) null del DE-LEVERING — obbligatorio su ogni claim di minor rischio")
for a in ASSETS:
sl_h, px_h = slope_hourly(curve, a), px_hourly(a)
s_d, r_d = _anchored(sl_h, px_h, 0)
d = pd.DataFrame({"slope": s_d, "r": r_d}).dropna()
vol = d["r"].rolling(14).std() * np.sqrt(365)
tgt = np.clip((d["slope"] > 0).astype(float) * (0.20 / vol.replace(0, np.nan)), 0, 2.0).fillna(0.0)
hold = tgt.shift(1).fillna(0.0)
net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna()
dd_c = A._dd_ret(net)
# ⚠️ il k giusto e' il PIU' GRANDE che sta sotto il DD del candidato, non il primo che
# trovi salendo da 0.05: partendo dal basso si confronta il candidato con un baseline
# quasi spento, che ha un DD ridicolo e uno Sharpe gonfiato dal rapporto. (Errore
# commesso e corretto in sessione: dava "candidato sopra il null" con k=0.05.)
ks = np.arange(0.05, 3.01, 0.05)
best = None
for k in ks[::-1]:
b = k * d["r"].reindex(net.index)
if A._dd_ret(b) <= dd_c:
best = (float(k), A._sh(b), A._dd_ret(b))
break
sh_c = A._sh(net)
if best is None:
print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | nessun k in "
f"[0.05,3.00] raggiunge quel DD")
else:
verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)"
if best[1] >= sh_c else "candidato sopra il null di de-levering")
FACTS[f"delev_{a}"] = bool(best[1] >= sh_c)
print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | buy&hold a leva "
f"k={best[0]:.2f} -> stesso o minor DD ({best[2]:.2%}) con Sharpe {best[1]:+.2f}"
f" => {verdict}")
imp = A.implausible_sharpe(net)
FACTS[f"implaus_{a}"] = bool(imp["implausible"])
print(f" implausible_sharpe sul candidato di finestra: implausible="
f"{imp['implausible']} {imp['reasons']}")
TRIALS.append((f"Q3 {a}: 60 valori di k nel null de-levering", 60))
# ===========================================================================================
# SEZIONE 5 — IL TEST LUNGO. La pendenza ha un PROXY con 5 anni di storia?
# ===========================================================================================
def _proxy_slope(df: pd.DataFrame, asset: str, rvwin: int = 7) -> np.ndarray:
"""Proxy A PRIORI della pendenza su storia lunga: DVOL(30g) - RV(rvwin), tutto causale.
Motivazione, non scelta a posteriori: iv_30 ~ DVOL (corr 0.99, SEZIONE 1) e iv_7 e' in larga
parte una funzione della RV recente (SEZIONE 2c). Il proxy e' quindi la pendenza con la gamba
corta sostituita dalla sua determinante. NON e' la stessa variabile: l'attenuazione e'
misurata sotto e va portata dietro a ogni conclusione.
"""
c = df["close"].to_numpy(float)
r = A.simple_returns(c)
bpd = A.bars_per_day(df)
rv = A.realized_vol(r, max(2, rvwin * bpd), bpd * 365.25) * 100.0
return A.dvol(df, asset) - rv
def make_proxy_dir(long_only: bool = True, thr: float = 0.0):
def fn(df, asset):
p = _proxy_slope(df, asset)
d = np.where(np.isfinite(p), np.where(p > thr, 1.0, 0.0 if long_only else -1.0), 0.0)
return A.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return fn
def sezione5(curve: pd.DataFrame) -> None:
hr("SEZIONE 5 — IL TEST LUNGO: la pendenza ha un proxy con 5 anni di storia?")
print(" Con 74 giorni nessun gate statistico ha potenza. L'unica via per DIRE qualcosa e' un")
print(" proxy a priori su storia lunga — e dichiararne l'attenuazione.")
sub("(a) quanto e' buono il proxy (misurato sulla finestra vera, non assunto)")
for a in ASSETS:
sl = slope_hourly(curve, a).resample("1D").last().dropna()
sl.index = pd.DatetimeIndex(sl.index).as_unit("ns")
df = A.get(a, "1d")
p = pd.Series(_proxy_slope(df, a),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))).dropna()
j = pd.concat({"vero": sl, "proxy": p}, axis=1, join="inner").dropna()
c = float(j.vero.corr(j.proxy))
print(f" {a}: corr(pendenza VERA, DVOL-RV7) = {c:+.3f} su n={len(j)} giorni "
f"-> attenuazione: il proxy spiega il {c**2:.0%} della varianza della pendenza")
print(" ⚠️ Un proxy a corr ~0.5-0.7 e' una lente SFOCATA: un risultato NEGATIVO sul proxy")
print(" e' evidenza piu' debole di un negativo sulla variabile vera. Va detto, non nascosto.")
sub("(b) il candidato direzionale sul proxy, 5 anni, coi gate veri")
fn = make_proxy_dir(long_only=True)
cau = A.causality_ok(fn, tf="1d")
print(f" causality_ok: {cau}")
cd = A.candidate_daily(fn, tf="1d")
era = cd[cd.index >= pd.Timestamp("2021-10-01", tz="UTC")]
print(f" era DVOL (2021-10 -> oggi, {len(era)} giorni): Sharpe {A._sh(era):+.3f} "
f"maxDD {A._dd_ret(era):.2%}")
rep = A.marginal_vs_tp01(cd)
FACTS["proxy_marginal"] = rep.get("marginal_verdict")
FACTS["proxy_sharpe"] = A._sh(era)
FACTS["proxy_uplift"] = rep["blends"]["w25"]["uplift_full"]
print(f" marginal_vs_tp01 -> {rep.get('marginal_verdict')} | corr a TP01 "
f"{rep.get('corr_full')} | uplift w25 full {rep['blends']['w25']['uplift_full']:+.3f} "
f"hold {rep['blends']['w25']['uplift_hold']}")
for k in ("is_hedge", "has_insample_edge", "robust_oos", "beats_noise_null", "earns_slot"):
if k in rep:
print(f" {k:20s} = {rep[k]}")
imp = A.implausible_sharpe(era)
print(f" implausible_sharpe (proxy, era DVOL): implausible={imp['implausible']} "
f"{imp['reasons']}")
print(f" earns_slot = (assoluto non-FAIL) AND (marginale ADDS): qui il marginale e' "
f"{rep.get('marginal_verdict')} -> **earns_slot = False**.")
TRIALS.append(("Sez.5: 2 varianti proxy (long-only, L/S) x 1 soglia", 2))
sub("(c) il precedente che questo replica")
print(" `dvol_directional.py` (2026-06-29) testo' VRP-Z = z-score causale di (IV-RV) come")
print(" segnale DIREZIONALE su 5 anni, con la griglia completa: verdetto **HEDGE,")
print(" earns_slot=False**. Qui, con la variante a SOGLIA BINARIA invece che a z-score,")
print(" il verdetto misurato e' NEUTRAL con uplift NEGATIVO — un verdetto diverso, la")
print(" stessa conclusione (earns_slot=False in entrambi). La differenza sta nella")
print(" variante, non nell'esito, e va riportata cosi': non sto dichiarando una replica")
print(" che non ho fatto.")
print(" Il punto strutturale resta: la pendenza e' il premio di vol con la gamba")
print(" REALIZZATA sostituita da una IMPLICITA (SEZIONE 2c: corr(pendenza, RV passata)")
print(" = -0.50/-0.57). Non e' una variabile nuova per questo progetto: e' quella, con")
print(" un rumore di misura piu' basso e 5 anni di storia in meno.")
# ===========================================================================================
# SEZIONE 6 — CONTI, GATE, VERDETTO
# ===========================================================================================
def sezione6() -> None:
hr("SEZIONE 6 — GRIGLIA DICHIARATA E DEFLATED SHARPE")
tot = sum(n for _, n in TRIALS)
for name, n in TRIALS:
print(f" {n:5d} {name}")
print(f" {'-'*70}\n {tot:5d} TOTALE (contato al RIALZO: ogni variante valutata, anche scartata)")
print("\n Non e' stata ridotta nessuna griglia per il budget: il vincolo di questo filone")
print(" non e' il tempo di calcolo, e' che il campione dura 74 giorni.")
print("\n deflated_sharpe: NON GIRATO su un candidato di questo studio, e il motivo e' una")
print(" scelta, non una dimenticanza: il DSR e' una correzione per multiple-testing su UNA")
print(" serie di ritorni; qui il candidato migliore ha 74 osservazioni e la sua incertezza")
print(" di CAMPIONE (SE(Sharpe) ~ 2.2) domina di un ordine di grandezza quella da selezione.")
print(f" Deflazionare {tot} trial su 74 giorni darebbe un numero preciso e privo di senso.")
print(" Sul proxy a 5 anni il DSR e' invece pertinente ed e' riportato dal gate del 29/06")
print(" su cui questo filone ricade (VRP-Z: earns_slot=False).")
# ===========================================================================================
# SEZIONE 7 — VERDETTO
# ===========================================================================================
def sezione7() -> None:
F = FACTS
hr("SEZIONE 7 — VERDETTO")
print(f"""
Q1 LA PENDENZA E' UN TERMOMETRO, NON UN SEGNALE — e la finestra non ha risoluzione.
· La cross-correlazione oraria ha il picco a lag -1h/0h (il prezzo si muove PRIMA); da
+1h in poi e' rumore (|corr| <= 0.05 su ogni lag fino a 24h).
· corr(pendenza, RV 24h PASSATA) = {F.get('conf_past_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_past_ETH', float('nan')):+.2f} (ETH)
corr(pendenza, RV 24h FUTURA) = {F.get('conf_fwd_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_fwd_ETH', float('nan')):+.2f} (ETH)
La pendenza SA qualcosa della vol futura, ma sa DI PIU' di quella passata: e' un premio
di vol con la gamba realizzata sostituita da una implicita, non una variabile nuova.
· L'unica correlazione col ritorno futuro che sembra qualcosa (h=5g: {F.get('c5_BTC', float('nan')):+.2f} BTC /
{F.get('c5_ETH', float('nan')):+.2f} ETH) NON SOPRAVVIVE alla rimozione dell'ULTIMA SETTIMANA:
diventa {F.get('c5_no7_BTC', float('nan')):+.2f} / {F.get('c5_no7_ETH', float('nan')):+.2f}. Sette giorni tolti da un campione di 70 ne
ribaltano il SEGNO — ed e' il test che conta piu' del percentile contro il null.
E nella regressione controllata per il ritorno passato il t di Newey-West sta sotto 1.0
a ogni orizzonte. Non e' "debole": e' sotto la risoluzione del campione.
Q2 IL CARRY C'E' MA NON PAGA IL BID-ASK, e non e' nemmeno incassato.
· Roll-down da corto: ~0.07 (BTC) / ~0.13 (ETH) punti-vol al GIORNO.
Round-trip ATM 10-20g: 1.26 (BTC) / 1.77 (ETH) punti-vol.
=> {F.get('giorni_carry_BTC', float('nan')):.0f} giorni (BTC) / {F.get('giorni_carry_ETH', float('nan')):.0f} giorni (ETH) di carry per pagare UN giro.
· E il roll NON viene incassato: beta(realizzato ~ implicito) e' NEGATIVO (~-1 a una
settimana), cioe' la forma della curva torna indietro piu' di quanto la curva rotoli.
La differenza realizzato-implicito e' dentro il rumore (|t| <= 0.9 a blocchi).
· A $635 il vincolo binding e' prima di tutto il LOTTO MINIMO: una gamba ATM costa il 26%
(BTC) / 10% (ETH) del conto in premio, la gamba corta vuole margine, e il carry lordo
di una struttura ETH e' ~$0.19/giorno. Il bersaglio dichiarato e' 50 EUR/giorno.
Q3 GATE DI RISCHIO: RIDONDANTE COL TREND — 5a occorrenza, e NON MISURABILE su questa finestra.
· TP01 e' FLAT nel {F.get('tp01_flat', float('nan')):.0%} dei giorni della finestra: non c'e' rischio da togliere.
Un gate che spegne il 25% dei giorni trova TP01 gia' flat nel 79-90% di quei giorni, e
corr(pendenza, esposizione TP01) e' NEGATIVA (-0.29 BTC / -0.52 ETH): la backwardation
arriva quando il trend e' gia' uscito. Firma identica ai 4 precedenti.
· Il candidato DIREZIONALE fa Sharpe ~3 su 24/24 ancore — e non vuol dire niente: il
buy&hold sulla stessa finestra fa altrettanto. Mediana delle DIFFERENZE APPAIATE contro
buy&hold: {F.get('vs_bh_BTC', float('nan')):+.2f} BTC (positiva in {F.get('vs_bh_n_BTC', (0, 0))[0]}/{F.get('vs_bh_n_BTC', (0, 0))[1]} ancore) e {F.get('vs_bh_ETH', float('nan')):+.2f} ETH ({F.get('vs_bh_n_ETH', (0, 0))[0]}/{F.get('vs_bh_n_ETH', (0, 0))[1]}).
· Null del de-levering: ETH {'NON superato' if F.get('delev_ETH') else 'superato'}, BTC {'NON superato' if F.get('delev_BTC') else 'superato'}.
· `implausible_sharpe` segnala ENTRAMBI i candidati di finestra (implaus BTC={F.get('implaus_BTC')},
ETH={F.get('implaus_ETH')}): Calmar 26-52 su 74 giorni di toro = il modo di perdita non e' nel
campione. Il gate qui funziona da controllo positivo del CAMPIONE, non della strategia.
TEST LUNGO (proxy DVOL-RV7, 5 anni): Sharpe {F.get('proxy_sharpe', float('nan')):+.2f}, marginale vs TP01 =
{F.get('proxy_marginal')}, uplift w25 {F.get('proxy_uplift', float('nan')):+.3f} (NEGATIVO), robust_oos=False
=> earns_slot = False. Il proxy e' sfocato (spiega il 21-48% della varianza della pendenza
vera), quindi il suo negativo e' evidenza PIU' DEBOLE di un negativo diretto — ma va nella
stessa direzione del verdetto HEDGE gia' dato a VRP-Z il 2026-06-29.
======================================================================================
VERDETTO: SCARTATO come sleeve e come gate. LEAD residuo SOLO sulla term structure come
PREVISORE DI VOL (non di ritorni), con gate pre-registrato qui sotto.
======================================================================================
Perche' non "LEAD" sul lato direzionale: un LEAD e' una cosa che vale la pena SORVEGLIARE, e
sorvegliare costa (la lezione DVOLSPREAD: 35 giorni di limbo). Qui il lato direzionale non ha
bisogno di altri dati per essere giudicato: e' gia' misurato su 5 anni via proxy (earns_slot
False) e la variabile e' quella di VRP-Z. Aprire un monitor sarebbe pagare due volte lo stesso
filone. Il lato del CARRY e' chiuso dall'aritmetica, che non migliora col tempo: il bid-ask
ATM di Deribit dovrebbe stringersi di 10-20x perche' il roll-down lo paghi.
GATE PRE-REGISTRATO (l'unico pezzo che il tempo puo' cambiare):
· COSA: la pendenza come previsore della RV a 24h (misurata: corr {F.get('conf_fwd_BTC', float('nan')):+.2f} / {F.get('conf_fwd_ETH', float('nan')):+.2f}).
Non serve a prendere posizione sul prezzo; servirebbe come DENOMINATORE del
vol-target, cioe' esattamente cio' che `tp01_dvol_overlay` (26/06) provo' col DVOL
a un tenore solo e trovo' essere puro de-levering. La curva a piu' tenori e' l'unica
variante di quell'esperimento mai fatta.
· QUANDO: 2027-02-01. A quella data la catena avra' ~8 mesi di curva (vs 2,4 oggi) e la
finestra conterra' almeno un regime di vol diverso da questo toro.
· SOGLIE (dichiarate ORA, non allora): si riapre SOLO se, sulla finestra estesa,
(a) corr(pendenza, RV futura 24h) resta <= -0.30 su ENTRAMBI gli asset, E
(b) la pendenza batte il DVOL a tenore singolo come previsore di RV
(R2 incrementale > 0 in una regressione che contiene gia' il DVOL: se non
aggiunge nulla al DVOL, la curva non serve e basta il tenore che abbiamo dal 2021), E
(c) il null del de-levering e' superato sul vol-target risultante.
Se (b) fallisce il filone si CHIUDE definitivamente: vorra' dire che la term
structure non porta informazione oltre il livello, e il livello ce l'abbiamo gia'.
· COSA MI SMENTIREBBE OGGI: una finestra che contenga un vero crash. Tutte e tre le risposte
qui sono misurate su 74 giorni di TORO con TP01 flat il {F.get('tp01_flat', float('nan')):.0%} del tempo. La backwardation
severa (pendenza < -5 pt-vol) copre il {F.get('back_sev_BTC', float('nan')):.1%} delle ore BTC e il {F.get('back_sev_ETH', float('nan')):.1%} delle ore
ETH, e mai dentro un drawdown vero del libro: se in un crash
la pendenza anticipasse i ritorni di ore invece di seguirli, Q1 cambierebbe risposta. Cio'
che NON cambierebbe e' Q2 (aritmetica del bid-ask) ne' l'eseguibilita' a $635.
""")
def main() -> None:
hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH")
chain = load_quoted_chain()
sezione0(chain)
atm_exp = build_atm_by_expiry(chain)
curve = build_curve(atm_exp)
sezione1(curve)
sezione2(curve)
sezione3(chain, atm_exp, curve)
del chain
sezione4(curve)
sezione5(curve)
sezione6()
sezione7()
hr("FINE")
if __name__ == "__main__":
main()
+867
View File
@@ -0,0 +1,867 @@
#!/usr/bin/env python
"""r0822_vol_size.py — CONTROLLO DEL RISCHIO SU SKH01 e VOL-TARGET A LIVELLO DI LIBRO.
DOMANDA (ondata 2026-08-22, filone VOL-SIZE). Il libro live Deribit e' TP01 75% + SKH01 25%,
nettati su un solo strumento. TP01 e' vol-targeted al 20% sulla vol REALIZZATA dell'asset;
**SKH01 non ha alcun controllo del rischio** — `backtest_signals(..., leverage=1.0,
position_size=1.0)`, verificato il 26/07: il suo 20,7% di vol realizzata e' un PRODOTTO della
strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro. E' l'unica delle
cinque gambe senza dimensionamento, la piu' fee-sensibile (~4x TP01) e la piu' fragile
all'ancora (LOO 26/07).
Q1 dare a SKH01 un dimensionamento del rischio (inverse-vol per-trade / rischio costante per
trade / vol-target sulla sua vol di strategia) migliora il LIBRO, o e' de-levering?
Q2 il vol-target va applicato al LIBRO gia' nettato invece che al singolo sleeve? Oggi ogni
sleeve si dimensiona da solo e poi si nettano: la versione book-level non e' mai stata
misurata.
COSA NON SI RIFA' (gia' misurato): TP01 x DVOL (26/06, de-levering); il peso 75/25 (confermato
3 volte). I pesi NOMINALI non si toccano; una modulazione di size e' pero' un cambio di pesi nel
tempo, quindi `weights_tilt_null` si riporta lo stesso (§7).
IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE: **de-levering su quasi tutto**. Ragione
dichiarata prima: lo Sharpe e' INVARIANTE a una costante moltiplicativa, quindi una variante che
si limiti ad abbassare la leva media non puo' cambiare lo Sharpe e puo' solo abbassare il DD —
la firma esatta del null del de-levering (5 occorrenze nel progetto). Per VINCERE una variante
deve cambiare la FORMA (quale trade pesa quanto), e la forma si vede solo sullo Sharpe.
Sotto-ipotesi: l'inverse-vol per-trade e' l'unica con una ragione strutturale (lo stop di SKH01
e' una PERCENTUALE FISSA 4%/2%, quindi il rischio in unita' di sigma varia col regime); il
vol-target sulla serie giornaliera dello sleeve e' quello che ha piu' probabilita' di rompersi
(SKH01 e' ~88% di zeri: la vol trailing di quella serie misura l'ATTIVITA', non il rischio).
⚠️ DIFETTO TROVATO NELLA MIA PRIMA STESURA, E MISURATO INVECE CHE CANCELLATO (§4). Il modo
ovvio di scrivere un vol-target su uno sleeve — moltiplicare la sua serie GIORNALIERA per
L_t = tv/rv_{t-1} — e' **non causale su SKH01**, perche' la sua equity e' a GRADINO: tutto il
P&L di un trade e' contabilizzato il giorno di CHIUSURA. Scalare quel giorno per L_t significa
aver tenuto size L_t dall'INGRESSO, che e' fino a ~4 giorni prima (24 barre da 230m). L_t usa
solo dati <= t-1 e quindi passa qualunque controllo di causalita' scritto sulla serie
giornaliera: il look-ahead sta nella CONTABILITA', non nella formula. La versione causale fissa
la size all'INGRESSO. Entrambe sono misurate qui, e la differenza e' un risultato.
LENTE E ANCORA. Path CANONICO (fill al livello, entry a chiusura di bin): e' la lente di tutti i
numeri pubblicati di SKH01 ed e' identica nelle due braccia di ogni confronto — il Delta e'
pulito anche se il livello assoluto e' pessimistico verso il path live (26/07: il live e'
migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a priori dell'audit
02/07 (ogni 30m su [0,690)); statistica = **mediana delle DIFFERENZE APPAIATE**
(`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in
entrambe le braccia — §6 verifica che il segno non dipenda da lei.
⚠️ IL CONTROLLO CHE DECIDE — ISO-PESO. Quasi ogni variante di size alza la VOL della gamba SKH,
e quindi il suo peso EFFETTIVO nel libro (i nominali restano 75/25). Ma "alzare il peso di SKH01"
e' gia' stato misurato e RESPINTO il 26/07 (argmax w=0.35, `weights_tilt_null` fallito: alzarlo non
compra Sharpe, compra dipendenza dall'ancora). Percio' il confronto pubblicato non e' vs il
baseline nudo ma vs il baseline **RI-SCALATO allo stesso peso effettivo** (colonne ISO): li' resta
solo la FORMA — quale trade pesa quanto. E' la regola dell'iso-rischio gia' codificata (25/07 §3).
Corollario: siccome lo Sharpe e' invariante alla scala, `sh(k*baseline) == sh(baseline)`, quindi il
null del de-levering in questa famiglia COINCIDE col confronto degli Sharpe. `k_iso` resta come
diagnostica di quanto si e' mosso il DD.
DECOMPOSIZIONE DEL MECCANISMO (famiglia ACT). La vol trailing di uno sleeve all'88% di zeri
contiene DUE canali: la MAGNITUDINE degli esiti e la FREQUENZA dei trade. `VTL ... attive` isola
il primo (vol sulle sole barre attive), `ACT` isola il secondo (conteggio di barre attive nella
finestra). Se il guadagno vive in ACT la variante non e' "controllo del rischio" ma un filtro di
frequenza, e va detto.
NULL DI PERMUTAZIONE (§10). Lo stesso MULTINSIEME di size riassegnato a caso ai trade: separa
"conta QUALE trade riceve quale size" (informazione) da "conta la DISTRIBUZIONE delle size".
PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata
anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline).
E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che
decide. Il null del de-levering (`k_iso_dd`, importato da r0822_sol_leg, non riscritto) e'
calcolato per ogni variante che riduce il DD.
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_vol_size.py
[--every K] 1 offset ogni K dei 23 (K=1 = tutti; pilota: --every 8)
"""
from __future__ import annotations
import argparse
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"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
import r0702_anchor_skh01 as R # noqa: E402 (run_asset/resample_off riusati)
import r0702_tp01_offset as TO # noqa: E402 (TP01 per ancora + target 1h)
from r0822_sol_leg import k_iso_dd # noqa: E402 (null de-levering, non riscritto)
from src.portfolio.portfolio import ( # noqa: E402
HOLDOUT, combine_outer, weights_tilt_null,
)
from src.backtest.harness import backtest_signals # noqa: E402
from src.strategies.skyhook import ( # noqa: E402
SKH01_V1, SKH01_V2_DD, skyhook_entries,
)
ASSETS = ("BTC", "ETH")
OFFSETS_FULL = tuple(range(0, 690, 30)) # 23 a priori: griglia dell'audit 02/07
LTF_MIN = 230
BARS_YR = 365.25 * 24 * 60 / LTF_MIN
DPY = 365.25
FEE_RT = 0.001
W_TP, W_SKH = 0.75, 0.25 # libro live Deribit
CAPITAL = 635.0 # conto reale dichiarato nel brief
MIN_ORDER = 5.0
CAP_ASSET = 0.5 # cap $318/asset su equity $635
SIG_WARM = 2000 # barre 230m (~1 anno) prima di dimensionare
VT_WARM = 250 # giorni prima di dimensionare su vol di strategia
# --------------------------------------------------------------------------- metriche
def sh(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0
def maxdd(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
if len(r) < 2:
return 0.0
eq = np.cumprod(1.0 + r)
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk))
def cagr(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
if len(r) < 2:
return 0.0
eq = float(np.prod(1.0 + r))
yrs = len(r) / DPY
return float(eq ** (1 / yrs) - 1) if eq > 0 and yrs > 0 else -1.0
def vol(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(r.std() * np.sqrt(DPY)) if len(r) > 2 else 0.0
def hold(s: pd.Series) -> pd.Series:
return s[s.index >= HOLDOUT]
def ins(s: pd.Series) -> pd.Series:
return s[s.index < HOLDOUT]
# --------------------------------------------------------------------------- estrazione trade
def extract(asset: str, off: int, params=None) -> dict:
"""Trade + contesto per (asset, offset). Replica il loop di `backtest_signals` con
leverage=1: stessa detection, stesso non-overlap, stessi fill al livello. Cio' che
aggiunge e' la SIZE per-trade, che l'harness non espone (ha un solo `position_size`
scalare). La §0 verifica bit-exact che con size=1 si ritrovi la serie ufficiale."""
if params is None or params is SKH01_V2_DD:
_, _, ltf, ent = R.run_asset(asset, off) # path canonico (sanity bit-exact §0)
else: # stessa pipeline, altra config Skyhook
df5 = R.get5m(asset)
ltf = R.resample_off(df5, R.LTF_MIN, off)
htf = R.resample_off(df5, R.HTF_MIN, off)
ent = skyhook_entries(ltf, htf, params)
c = ltf["close"].values.astype(float)
h = ltf["high"].values.astype(float)
lo = ltf["low"].values.astype(float)
n = len(c)
i_ent, i_ex, dirs, nets, stops = [], [], [], [], []
busy = -1
for i in range(n):
e = ent[i] if i < len(ent) else None
if e is None or e.get("dir", 0) == 0 or i <= busy:
continue
d = int(e["dir"])
entry = c[i]
tp, sl = e.get("tp"), e.get("sl")
mb = int(e.get("max_bars") or 24)
ex_i = min(i + mb, n - 1)
ex_p = c[ex_i]
for j in range(i + 1, min(i + mb + 1, n)):
hit_sl = sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl))
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp))
if hit_sl:
ex_p, ex_i = sl, j
break
if hit_tp:
ex_p, ex_i = tp, j
break
ex_p, ex_i = c[j], j
i_ent.append(i); i_ex.append(ex_i); dirs.append(d)
nets.append((ex_p - entry) / entry * d - FEE_RT)
stops.append(abs(float(sl) - entry) / entry) # DERIVATO dal segnale, non ridichiarato
busy = ex_i
idxE = np.asarray(i_ent, int)
lr = A.log_returns(c)
sig, sigref = {}, {}
for w in (30, 90):
sv = A.realized_vol(lr, w, BARS_YR) # finestra che termina in i: causale
sig[w] = sv[idxE] if len(idxE) else np.array([])
sigref[w] = _expanding_median_at(sv, idxE)
idx = pd.DatetimeIndex(pd.to_datetime(ltf["timestamp"].values, unit="ms", utc=True))
out = dict(
n=n, idx=idx, i_ent=idxE, i_ex=np.asarray(i_ex, int), dir=np.asarray(dirs, int),
net=np.asarray(nets, float), stop=np.asarray(stops, float), sig=sig, sigref=sigref,
ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000,
day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"),
)
out["base_daily"] = equity_daily(out, size_flat(out)) # riusata da VTL/§9: calcolarla 1 volta
R._CACHE.clear() # ent = 18k dict per chiave: non accumulare
return out
def _expanding_median_at(v: np.ndarray, idxE: np.ndarray) -> np.ndarray:
"""Mediana ESPANDENTE di v fino a i incluso, valutata solo agli indici d'ingresso.
Causale per costruzione. NaN prima del warm-up."""
out = np.full(len(idxE), np.nan)
for k, i in enumerate(idxE):
if i < SIG_WARM:
continue
x = v[: i + 1]
x = x[np.isfinite(x)]
if len(x) >= SIG_WARM // 2:
out[k] = float(np.median(x))
return out
def equity_daily(ex: dict, sizes: np.ndarray) -> pd.Series:
"""Serie giornaliera dei rendimenti dalla tabella trade, con size per-trade. Convenzione
IDENTICA a `backtest_signals` + `_skyhook_returns`: equity a gradino a fine trade,
resample 1D last, ffill, pct_change."""
n = ex["n"]
eq = np.full(n, 1000.0, float)
cap = 1000.0
for k in range(len(ex["i_ent"])):
cap += cap * float(sizes[k]) * float(ex["net"][k])
cap = max(cap, 1.0)
eq[ex["i_ent"][k]: ex["i_ex"][k] + 1] = cap
for k in range(1, n): # stessi due riempimenti dell'harness
if eq[k] == 1000.0 and eq[k - 1] != 1000.0:
eq[k] = eq[k - 1]
last = 1000.0
for k in range(n):
if eq[k] != last and eq[k] != 1000.0:
last = eq[k]
else:
eq[k] = last
return pd.Series(eq, index=ex["idx"]).resample("1D").last().ffill().pct_change().dropna()
def leg_daily(exs: dict, sizes: dict) -> pd.Series:
"""Sleeve SKH01 50/50 BTC+ETH (convenzione di `_skyhook_returns`: inner join)."""
ser = {a: equity_daily(exs[a], sizes[a]) for a in ASSETS}
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)
# --------------------------------------------------------------------------- famiglie di size
def size_flat(ex: dict) -> np.ndarray:
return np.ones(len(ex["i_ent"]))
def size_iv(p: float, w: int, cap: float):
"""INVERSE-VOL per-trade sulla vol dell'ASSET: size = clip((sig_rif/sig_ingresso)^p, 1/cap, cap).
sig_rif = mediana ESPANDENTE causale -> size media ~1: la variante e' neutra alla leva,
quindi qualunque effetto sullo Sharpe e' FORMA, non de-levering."""
def f(ex: dict) -> np.ndarray:
s, r = ex["sig"][w], ex["sigref"][w]
with np.errstate(divide="ignore", invalid="ignore"):
raw = np.where((s > 0) & np.isfinite(r), (r / np.maximum(s, 1e-12)) ** p, 1.0)
return np.clip(np.nan_to_num(raw, nan=1.0, posinf=cap, neginf=1.0), 1.0 / cap, cap)
return f
def size_risk(p: float):
"""RISCHIO COSTANTE PER TRADE: size ~ 1/distanza-dallo-stop. Lo stop di SKH01 e' una
percentuale FISSA (4% long / 2% short) -> questa famiglia e' di fatto 'gli short pesano
2x i long'. La costante e' irrilevante (lo Sharpe e' scale-invariante)."""
def f(ex: dict) -> np.ndarray:
st = np.maximum(ex["stop"], 1e-9)
return np.where(ex["stop"] > 0, (0.04 / st) ** p, 1.0)
return f
def _rv_daily(s: pd.Series, w: int, active_only: bool) -> pd.Series:
"""Vol realizzata trailing di una serie GIORNALIERA, SFASATA di 1 giorno (causale al giorno t).
active_only=True la calcola sulle sole barre ATTIVE e la riporta sulla griglia di calendario:
SKH01 e' ~88% di zeri, e la versione 'tutte le barre' misura l'ATTIVITA', non il rischio."""
if not active_only:
return (s.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)).shift(1)
act = s[s != 0.0]
rv = act.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)
return rv.reindex(s.index).ffill().shift(1)
def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float,
active_only: bool = False) -> pd.Series:
"""L_t da una serie giornaliera di riferimento. tv=None -> versione NEUTRA ALLA LEVA
(rif = mediana espandente causale di rv), tv=float -> vol-target assoluto."""
rv = _rv_daily(base, w, active_only).replace(0.0, np.nan)
if tv is None:
ref = rv.expanding(min_periods=VT_WARM).median()
L = (ref / rv).clip(lower=1.0 / cap, upper=cap)
else:
L = (tv / rv).clip(lower=0.0, upper=cap)
return L.fillna(1.0)
def activity_L(base: pd.Series, w: int, cap: float) -> pd.Series:
"""L_t da sola ATTIVITA': quante barre ATTIVE (trade chiusi) negli ultimi w giorni, riferita
alla mediana espandente causale. Serve a separare i DUE canali dentro `rv` calcolata su tutte
le barre di uno sleeve all'88% di zeri: la MAGNITUDINE degli esiti e la FREQUENZA dei trade.
Se il guadagno vive qui, la variante non e' 'vol-target' ma un filtro di frequenza."""
act = (base != 0.0).astype(float)
cnt = act.rolling(w, min_periods=max(5, w // 3)).sum().shift(1).replace(0.0, np.nan)
ref = cnt.expanding(min_periods=VT_WARM).median()
return (ref / cnt).clip(lower=1.0 / cap, upper=cap).fillna(1.0)
def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray:
"""La size di ogni trade = L al giorno d'INGRESSO. E' la versione CAUSALE del vol-target su
uno sleeve a equity a gradino: L al giorno di CHIUSURA non era nota all'ingresso."""
if not len(ex["i_ent"]):
return np.array([])
v = L.reindex(ex["day_ent"]).values.astype(float)
return np.nan_to_num(v, nan=1.0)
# --------------------------------------------------------------------------- libro
def sizes_of(nm, ex: dict, SZ: dict, VTL: dict, ACT: dict, Lbook: pd.Series | None = None):
"""Vettore di size per-trade di una cella qualsiasi. Un solo posto in cui e' definita:
§8 (eseguibilita'), §9 (deriva) e §10 (null di permutazione) devono usare LA STESSA."""
if nm is None or nm == "BASE":
return size_flat(ex)
if nm in SZ:
return SZ[nm](ex)
if nm in VTL:
return sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
if nm in ACT:
return sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm]))
return sizes_from_L(ex, Lbook)
def book(tp: pd.Series, skh: pd.Series) -> pd.Series:
return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH})
def iso_vol(s: pd.Series, ref: pd.Series) -> pd.Series:
v = vol(s)
return s * (vol(ref) / v) if v > 0 else s
HEAD = (f" {'variante':<28}{'ShFULL':>8}{'ShHOLD':>9}{'ShIS':>8}"
f"{'vol':>9}{'maxDD':>9}{'DDisovol':>10}{'CAGRiso':>9}{'size med':>10}")
def transfer_test(offs) -> None:
"""TEST DI TRASFERIMENTO — la regola di size CONGELATA (tv=0.20, w=90, cap 3, fissata
all'INGRESSO) applicata a SKH01_**V1**, l'altra config Skyhook: stesso motore, stessi asset,
ma geometria d'uscita DIVERSA (stop/target in ATR 2.5/6.0 invece che a percentuale fissa
4%/2%, ptn_n=55 invece di 45). E' il gate giusto per questo filone: un forward di 6 mesi non
puo' misurare un dSharpe di 0.07 (SE ~1.4), un trasferimento si'. Se il meccanismo e'
'pareggiare il contributo di rischio dei trade' deve funzionare anche li'; se e' cucito sulla
sequenza di trade di V2-DD, no. NB: V1 e' l'unica config alternativa gia' nel repo e non
scelta da me — non e' una griglia nuova."""
print("=" * 112)
print(" TEST DI TRASFERIMENTO — regola di size CONGELATA da V2-DD, applicata a SKH01_V1")
print("=" * 112)
TP = A.tp01_baseline_daily()
sp = dict(tv=0.20, w=90, cap=3.0, active_only=False)
base_b, var_b, ctrl_b = {}, {}, {}
for o in offs:
ex = {a: extract(a, o, SKH01_V1) for a in ASSETS}
lb = leg_daily(ex, {a: size_flat(ex[a]) for a in ASSETS})
lv = leg_daily(ex, {a: sizes_from_L(ex[a], leverage_series(ex[a]["base_daily"], **sp))
for a in ASSETS})
m_s = vol(lv) / vol(lb) if vol(lb) > 0 else 1.0
base_b[o], var_b[o] = book(TP, lb), book(TP, lv)
ctrl_b[o] = book(TP, lb * m_s)
dF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: ctrl_b[o], offs, metric=sh)
dH = A.anchor_luck_delta(lambda o: hold(var_b[o]), lambda o: hold(ctrl_b[o]), offs, metric=sh)
rF = A.anchor_luck_delta(lambda o: var_b[o], lambda o: base_b[o], offs, metric=sh)
print(f" trade V1 medi/ancora: " + ", ".join(
f"{a} {int(np.mean([len(extract(a, offs[0], SKH01_V1)['i_ent'])]))}" for a in ASSETS))
print(f" libro V1 BASE, mediana della banda: ShFULL "
f"{np.median([sh(base_b[o]) for o in offs]):.3f} / ShHOLD "
f"{np.median([sh(hold(base_b[o])) for o in offs]):.3f}")
print(f" dShFULL grezzo {rF['median_paired']:+.3f} ({rF['n_positive']}/{len(offs)} ancore)")
print(f" dShFULL ISO-PESO {dF['median_paired']:+.3f} ({dF['n_positive']}/{len(offs)} ancore)"
f" banda [{dF['lo']:+.3f}, {dF['hi']:+.3f}]")
print(f" dShHOLD ISO-PESO {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)} ancore)")
yrs = sorted({int(y) for y in base_b[offs[0]].index.year})
cells = []
for y in yrs:
v = [sh(var_b[o][var_b[o].index.year == y]) - sh(ctrl_b[o][ctrl_b[o].index.year == y])
for o in offs if (var_b[o].index.year == y).sum() > 20]
cells.append(float(np.median(v)) if v else float("nan"))
print(" per anno (ISO): " + "".join(f"{y:>8}" for y in yrs))
print(" " + "".join(f"{c:>+8.2f}" for c in cells) +
f" positivi {int(np.nansum(np.asarray(cells) > 0))}/{len(yrs)}")
print("\n LETTURA: se il segno regge qui, il meccanismo non e' cucito su V2-DD. Se non regge,")
print(" la regola e' una proprieta' della sequenza di trade su cui e' stata trovata.")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--every", type=int, default=1)
ap.add_argument("--transfer", action="store_true",
help="esegue SOLO il test di trasferimento su SKH01_V1")
args = ap.parse_args()
if args.transfer:
transfer_test(OFFSETS_FULL[:: max(1, args.every)])
return
offs = OFFSETS_FULL[:: max(1, args.every)]
t0 = time.time()
print("=" * 112)
print(" r0822 VOL-SIZE — controllo del rischio su SKH01 (Q1) e vol-target di LIBRO (Q2)")
print("=" * 112)
print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} offset a priori {list(offs)}")
print(" lente: path CANONICO. Ancora TP01: canonica in ENTRAMBE le braccia (§6 la varia).")
print("\n estrazione tabelle trade ...", flush=True)
EX = {o: {a: extract(a, o) for a in ASSETS} for o in offs}
print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " +
", ".join(f"{a} {int(np.mean([len(EX[o][a]['i_ent']) for o in offs]))}" for a in ASSETS))
# ------------------------------------------------------------------ §0 SANITY
print("\n" + "-" * 112)
print(" 0. SANITY — con size=1 la ricomposizione DEVE riprodurre la serie ufficiale bit-exact")
print(" (se non e' 0, ogni numero sotto e' di un'altra strategia)")
print("-" * 112)
worst = 0.0
for o in offs[: min(3, len(offs))]:
for a in ASSETS:
mine = equity_daily(EX[o][a], size_flat(EX[o][a]))
ref = R.run_asset(a, o)[0]
R._CACHE.clear()
assert len(mine) == len(ref), f"len {len(mine)} vs {len(ref)} ({a},{o})"
d = float(np.max(np.abs(mine.values - ref.values)))
worst = max(worst, d)
print(f" {a} off{o:>3}: max|dif| = {d:.2e} ({len(mine)} giorni)")
assert worst < 1e-15, f"ricomposizione NON bit-exact: {worst:.2e}"
if 0 in EX:
from src.portfolio.sleeves import _skyhook_returns
s0 = leg_daily(EX[0], {a: size_flat(EX[0][a]) for a in ASSETS})
d0 = float(np.max(np.abs(s0.values - _skyhook_returns().values)))
print(f" sleeve 50/50 off0 vs sleeves._skyhook_returns(): max|dif| = {d0:.2e}")
assert d0 < 1e-15
# ------------------------------------------------------------------ §1 CAUSALITA'
print("\n" + "-" * 112)
print(" 1. CAUSALITA' — ogni size ricalcolata TRONCANDO i dati alla barra d'ingresso")
print("-" * 112)
ex = EX[offs[0]]["BTC"]
_, _, ltf, _ = R.run_asset("BTC", offs[0])
c_full = ltf["close"].values.astype(float)
R._CACHE.clear()
full_sz = size_iv(1.0, 30, 3.0)(ex)
sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= SIG_WARM][:40]
bad = 0
for k in sel:
i = ex["i_ent"][k]
sv_t = A.realized_vol(A.log_returns(c_full[: i + 1]), 30, BARS_YR)
s_t = sv_t[i]
r_t = float(np.median(sv_t[np.isfinite(sv_t)]))
sz_t = np.clip((r_t / s_t) if s_t > 0 else 1.0, 1 / 3.0, 3.0)
bad += int(abs(sz_t - full_sz[k]) > 1e-9)
print(f" IV: {len(sel)-bad}/{len(sel)} size identiche a dati troncati (divergenze {bad})")
assert bad == 0, "la size IV usa dati futuri"
lag = np.array([(ex["idx"][ex["i_ex"][k]] - ex["idx"][ex["i_ent"][k]]).total_seconds() / 86400
for k in range(len(ex["i_ent"]))])
print(f" durata dei trade (giorni): mediana {np.median(lag):.2f}, p90 {np.percentile(lag,90):.2f}, "
f"max {lag.max():.2f} <- e' l'ampiezza del look-ahead della versione NAIVE (§4)")
# ------------------------------------------------------------------ griglia
SZ: dict = {}
for p in (0.5, 1.0):
for w in (30, 90):
for cp in (2.0, 4.0):
SZ[f"IV p{p} w{w} cap{cp:.0f}"] = size_iv(p, w, cp)
for p in (0.5, 1.0):
SZ[f"RISK p{p}"] = size_risk(p)
VTL = {} # vol-target sulla vol di STRATEGIA, causale
for w in (30, 90):
VTL[f"VTL neutro w{w}"] = dict(tv=None, w=w, cap=3.0, active_only=False)
VTL[f"VTL tv20 w{w}"] = dict(tv=0.20, w=w, cap=3.0, active_only=False)
VTL["VTL neutro w90 attive"] = dict(tv=None, w=90, cap=3.0, active_only=True)
VTL["VTL tv20 w90 attive"] = dict(tv=0.20, w=90, cap=3.0, active_only=True)
BVT = {} # Q2: vol-target del LIBRO
for w in (30, 90, 180):
for cp in (2.0, 3.0):
BVT[f"BOOKVT w{w} cap{cp:.0f}"] = dict(tv=0.10, w=w, cap=cp)
ACT = {f"ACT w{w}": dict(w=w, cap=3.0) for w in (90, 180)}
NAIVE = {f"NAIVE-VT w{w}": dict(tv=0.20, w=w, cap=3.0, active_only=False) for w in (30, 90)}
ALL = list(SZ) + list(VTL) + list(ACT) + list(BVT) + list(NAIVE)
print(f"\n GRIGLIA DICHIARATA: {len(SZ)} size per-trade + {len(VTL)} vol-target di gamba + "
f"{len(ACT)} filtro-attivita' + {len(BVT)} vol-target di libro + {len(NAIVE)} controlli "
f"non-causali = {len(ALL)} celle (+ baseline).")
# ------------------------------------------------------------------ serie
TP = A.tp01_baseline_daily()
SLE = {"BASE": {}}
BK = {"BASE": {}}
MTP = {"BASE": {o: 1.0 for o in offs}} # rapporto di vol della gamba TP01
SIZEMED = {}
for o in offs:
SLE["BASE"][o] = leg_daily(EX[o], {a: size_flat(EX[o][a]) for a in ASSETS})
BK["BASE"][o] = book(TP, SLE["BASE"][o])
for nm, fn in SZ.items():
SLE[nm] = {o: leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) for o in offs}
BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs}
MTP[nm] = {o: 1.0 for o in offs}
SIZEMED[nm] = float(np.median(np.concatenate([fn(EX[offs[0]][a]) for a in ASSETS])))
for nm, sp in VTL.items():
SLE[nm], BK[nm], MTP[nm] = {}, {}, {}
for o in offs:
sz = {}
for a in ASSETS:
sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp))
SLE[nm][o] = leg_daily(EX[o], sz)
BK[nm][o] = book(TP, SLE[nm][o])
MTP[nm][o] = 1.0
if o == offs[0]:
SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values()))))
for nm, sp in ACT.items():
SLE[nm], BK[nm], MTP[nm] = {}, {}, {}
for o in offs:
sz = {a: sizes_from_L(EX[o][a], activity_L(EX[o][a]["base_daily"], **sp))
for a in ASSETS}
SLE[nm][o] = leg_daily(EX[o], sz)
BK[nm][o] = book(TP, SLE[nm][o])
MTP[nm][o] = 1.0
if o == offs[0]:
SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values()))))
for nm, sp in BVT.items():
SLE[nm], BK[nm], MTP[nm] = {}, {}, {}
for o in offs:
L = leverage_series(BK["BASE"][o], sp["tv"], sp["w"], sp["cap"])
sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS}
SLE[nm][o] = leg_daily(EX[o], sz)
tp_s = TP * L.reindex(TP.index).ffill().fillna(1.0)
BK[nm][o] = book(tp_s, SLE[nm][o])
MTP[nm][o] = vol(tp_s) / vol(TP)
if o == offs[0]:
SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values()))))
for nm, sp in NAIVE.items(): # controllo NON causale (vedi §4)
SLE[nm] = {o: SLE["BASE"][o] * leverage_series(SLE["BASE"][o], **sp) for o in offs}
BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs}
MTP[nm] = {o: 1.0 for o in offs}
SIZEMED[nm] = float(np.median(leverage_series(SLE["BASE"][offs[0]], **sp).values))
# CONTROLLO ISO-PESO: ogni variante che alza la vol della gamba SKH alza il suo peso EFFETTIVO
# nel libro — e "alzare il peso di SKH01" e' gia' stato misurato (26/07: argmax w=0.35, ma
# `weights_tilt_null` FALLITO e proposta respinta). Quindi il confronto che conta NON e' vs il
# baseline nudo ma vs il baseline RI-SCALATO agli stessi rapporti di vol: cosi' resta solo la
# FORMA (quale trade pesa quanto). E' la regola dell'ISO-RISCHIO gia' codificata (25/07 §3).
CTRL, WEFF = {}, {}
for nm in ALL:
CTRL[nm], WEFF[nm] = {}, {}
for o in offs:
m_s = vol(SLE[nm][o]) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0
m_t = MTP[nm][o]
CTRL[nm][o] = book(TP * m_t, SLE["BASE"][o] * m_s)
WEFF[nm][o] = W_SKH * m_s / (W_TP * m_t + W_SKH * m_s)
# ------------------------------------------------------------------ §2 sleeve
can = offs[0]
print("\n" + "-" * 112)
print(f" 2. SLEEVE SKH01 all'ancora canonica (off {can}). 'DDisovol'/'CAGRiso' a vol pareggiata")
print(" col baseline (post-hoc, dichiarato: lo Sharpe non ne dipende).")
print("-" * 112)
print(HEAD)
base_s = SLE["BASE"][can]
def prow(nm, s, med=None):
iv = iso_vol(s, base_s)
m = f"{med:>10.2f}" if med is not None else f"{'-':>10}"
print(f" {nm:<28}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{sh(ins(s)):>8.3f}{vol(s)*100:>8.1f}%"
f"{maxdd(s)*100:>8.1f}%{maxdd(iv)*100:>9.1f}%{cagr(iv)*100:>8.1f}%{m}")
prow("BASELINE (size fissa)", base_s, 1.00)
for nm in list(SZ) + list(VTL) + list(ACT) + list(NAIVE):
prow(nm, SLE[nm][can], SIZEMED.get(nm))
# ------------------------------------------------------------------ §3 libro, banda appaiata
print("\n" + "-" * 112)
print(f" 3. LIBRO 75/25 — mediana delle DIFFERENZE APPAIATE su {len(offs)} ancore, + null del")
print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)")
print("-" * 112)
print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}"
f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}"
f"{'null de-lev':>14}")
base_b = BK["BASE"]
RES = {}
for nm in ALL:
dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o: base_b[o], offs, metric=sh)
dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), lambda o: hold(base_b[o]),
offs, metric=sh)
iF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CTRL[n][o],
offs, metric=sh)
iH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]),
lambda o, n=nm: hold(CTRL[n][o]), offs, metric=sh)
dd = float(np.median([maxdd(BK[nm][o]) - maxdd(base_b[o]) for o in offs])) * 100
if maxdd(BK[nm][can]) < maxdd(base_b[can]) - 1e-6:
k = k_iso_dd(base_b[can], BK[nm][can])
shk = sh(k * base_b[can])
ks = f"{k:.3f}"
vd = "DE-LEVERING" if shk >= sh(BK[nm][can]) else "oltre-null"
else:
ks, vd = "n/a", "DD peggiore"
we = float(np.median([WEFF[nm][o] for o in offs]))
RES[nm] = dict(dF=dF["median_paired"], nF=dF["n_positive"], dH=dH["median_paired"],
nH=dH["n_positive"], iF=iF["median_paired"], niF=iF["n_positive"],
iH=iH["median_paired"], niH=iH["n_positive"], dd=dd, k=ks,
verdict=vd, weff=we)
print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}"
f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}"
f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}"
f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}"
f"{vd:>14}")
print(" ⚠️ NOTA sul null del de-levering: `sh(k*base)` e' IDENTICO a `sh(base)` perche' lo")
print(" Sharpe e' invariante a una costante -> in questa forma il null COINCIDE col confronto")
print(" degli Sharpe, ed e' per questo che qui lo Sharpe e' la metrica che decide e il DD no.")
print(" 'k_iso' resta come diagnostica: quanta leva dovrebbe cedere il baseline per pareggiare")
print(" il maxDD della variante. 'DE-LEVERING' = la variante NON batte quel baseline ridotto.")
print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo")
print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:")
print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.")
med_F = float(np.median([sh(base_b[o]) for o in offs]))
med_H = float(np.median([sh(hold(base_b[o])) for o in offs]))
med_D = float(np.median([maxdd(base_b[o]) for o in offs]))
print(f"\n libro BASE, ancora canonica: ShFULL {sh(base_b[can]):.3f} / ShHOLD "
f"{sh(hold(base_b[can])):.3f} / maxDD {maxdd(base_b[can])*100:.1f}% / "
f"CAGR {cagr(base_b[can])*100:.1f}%")
print(f" libro BASE, stima ONESTA (mediana della banda): ShFULL {med_F:.3f} / "
f"ShHOLD {med_H:.3f} / maxDD {med_D*100:.1f}%")
# ------------------------------------------------------------------ §4 il difetto misurato
print("\n" + "-" * 112)
print(" 4. IL DIFETTO MISURATO — vol-target NAIVE (scala il giorno di CHIUSURA) vs CAUSALE")
print(" (size fissata all'INGRESSO). Stessa formula di L, stessa finestra: cambia solo")
print(" QUANDO la si applica. La differenza e' il valore del look-ahead di contabilita'.")
print("-" * 112)
print(f" {'coppia':<34}{'dShFULL naive':>15}{'dShFULL causale':>17}{'phantom':>10}")
for w in (30, 90):
n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}"
a1, a2 = RES[n_nm]["dF"], RES[c_nm]["dF"]
print(f" {'vol-target tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}")
for w in (30, 90):
n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}"
a1, a2 = RES[n_nm]["dH"], RES[c_nm]["dH"]
print(f" {' (hold-out) tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}")
# ------------------------------------------------------------------ §5 selezione + DSR
print("\n" + "-" * 112)
print(" 5. SELEZIONE AL BUIO (solo pre-2025, mediana di banda) + deflated-Sharpe")
print("-" * 112)
CAUSAL = [n for n in ALL if n not in NAIVE]
is_med = {n: float(np.median([sh(ins(BK[n][o])) for o in offs])) for n in CAUSAL}
ho_med = {n: float(np.median([sh(hold(BK[n][o])) for o in offs])) for n in CAUSAL}
fu_med = {n: float(np.median([sh(BK[n][o]) for o in offs])) for n in CAUSAL}
base_is = float(np.median([sh(ins(base_b[o])) for o in offs]))
print(f" baseline in-sample (mediana banda): {base_is:.3f}")
for n in sorted(CAUSAL, key=lambda x: -is_med[x])[:6]:
print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}")
best = max(CAUSAL, key=lambda x: is_med[x])
print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})")
iso_is = {n: float(np.median([sh(ins(BK[n][o])) - sh(ins(CTRL[n][o])) for o in offs]))
for n in CAUSAL}
best_iso = max(CAUSAL, key=lambda x: iso_is[x])
print(f" cella scelta AL BUIO **a ISO-PESO**: {best_iso} (dIS iso {iso_is[best_iso]:+.3f})")
print(f" la stessa cella a iso-peso: dShFULL {RES[best]['iF']:+.3f} "
f"({RES[best]['niF']}/{len(offs)}), dShHOLD {RES[best]['iH']:+.3f} "
f"({RES[best]['niH']}/{len(offs)})")
trials = [fu_med[n] for n in CAUSAL] + [med_F]
dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can])
dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can])
print(f" DSR candidato {dsr:.3f} / DSR BASELINE {dsr_b:.3f} (null max-Sharpe {sr0:.3f}, "
f"N={len(trials)})")
print(f" dispersione degli {len(trials)} trial: sd {np.std(trials, ddof=1):.4f} -> "
f"⚠️ il DSR e' QUASI VACUO su una famiglia di perturbazioni della stessa strategia")
print(" (penalizza in proporzione alla varianza fra i trial: qui il baseline stesso lo")
print(" passa). Il gate che decide e' la banda appaiata del §3, non questo.")
# ------------------------------------------------------------------ §6 ancora TP01
print("\n" + "-" * 112)
print(" 6. CONTROLLO — l'ancora di TP01 e' tenuta fissa: il segno del Delta dipende da lei?")
print("-" * 112)
cand6 = [best] + [n for n in (list(SZ)[:0] + ["RISK p1.0"] + list(BVT)[:2]) if n != best]
print(f" {'variante':<28}" + "".join(f"{'TP h=%d' % h:>12}" for h in (0, 8, 16)))
for nm in cand6:
cells = []
for h in (0, 8, 16):
tph = TO.port_daily_native(h) if h else TP
if nm in BVT:
arm = {}
for o in offs:
L = leverage_series(book(tph, SLE["BASE"][o]), BVT[nm]["tv"], BVT[nm]["w"],
BVT[nm]["cap"])
sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS}
arm[o] = book(tph * L.reindex(tph.index).ffill().fillna(1.0),
leg_daily(EX[o], sz))
else:
arm = {o: book(tph, SLE[nm][o]) for o in offs}
bse = {o: book(tph, SLE["BASE"][o]) for o in offs}
d = A.anchor_luck_delta(lambda o, x=arm: x[o], lambda o, y=bse: y[o], offs, metric=sh)
cells.append(d["median_paired"])
print(f" {nm:<28}" + "".join(f"{v:>+12.3f}" for v in cells))
# ------------------------------------------------------------------ §7 gate
print("\n" + "-" * 112)
print(" 7. GATE — marginal_vs_tp01 / implausible_sharpe / weights_tilt_null")
print("-" * 112)
best_sleeve = max([n for n in list(SZ) + list(VTL) + list(ACT)], key=lambda x: is_med[x])
for nm in ("BASE", best_sleeve):
m = A.marginal_vs_tp01(SLE[nm][can])
b25 = m.get("blends", {}).get("w25", {})
print(f" marginal {nm:<26} {m.get('marginal_verdict')} corr {m.get('corr_full')} "
f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')}")
nlos = int((EX[can]["BTC"]["net"] < 0).sum() + (EX[can]["ETH"]["net"] < 0).sum())
ntot = int(len(EX[can]["BTC"]["net"]) + len(EX[can]["ETH"]["net"]))
for nm in ("BASE", best_sleeve):
imp = A.implausible_sharpe(SLE[nm][can], n_trades=ntot, n_losing_trades=nlos)
print(f" implausible {nm:<25} implausible={imp['implausible']} "
f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive "
f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}")
cols = {"TP01": TP, "SKH01": SLE["BASE"][can]}
for nm in dict.fromkeys([best, best_sleeve] + list(BVT)[:2]):
m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can])
wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s}
g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp,
caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL))
print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} "
f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} "
f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}")
print(" LIMITE DICHIARATO: il gate confronta vettori di pesi STATICI; della modulazione")
print(" cattura la sola componente di SCALA. La FORMA la giudica la banda appaiata (§3).")
# ------------------------------------------------------------------ §8 eseguibilita'
print("\n" + "-" * 112)
print(f" 8. ESEGUIBILITA' a ${CAPITAL:.0f} — min-order ${MIN_ORDER:.0f}, cap {CAP_ASSET:.0%}"
f" equity/asset, target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h (cio' che il cron manda)")
print("-" * 112)
print(f" {'configurazione':<28}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}"
f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}")
todo = [("BASE (size fissa)", None), (best_sleeve, best_sleeve), (list(BVT)[0], list(BVT)[0])]
for label, nm in todo:
tm = tr = tu = 0.0
no = ns = 0
for a in ASSETS:
df1h = TO.get1h(a)
tgt = netted_target(a, EX[can][a], nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h)
ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER)
tm += 0.5 * ev["modeled"]["sharpe"]
tr += 0.5 * ev["realistic"]["sharpe"]
tu += ev["executed_turnover_per_year"]
no += ev["n_executed_trades"]
dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0))
ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum())
print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}")
print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.")
# ------------------------------------------------------------------ §9 per anno + deriva
print("\n" + "-" * 112)
print(" 9. SCOMPOSIZIONE PER ANNO del dSharpe a ISO-PESO (regola codificata dopo SOL, 22/08:")
print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size")
print("-" * 112)
yrs = sorted({int(y) for y in BK["BASE"][can].index.year})
best_iv = max([n for n in SZ if n.startswith("IV")], key=lambda x: RES[x]["iF"])
best_rk = max([n for n in SZ if n.startswith("RISK")], key=lambda x: RES[x]["iF"])
top = list(dict.fromkeys([best, best_iso, best_iv, best_rk,
max(ACT, key=lambda x: RES[x]["iF"]),
max(BVT, key=lambda x: RES[x]["iF"])]))
print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs))
for nm in top:
cells = []
for y in yrs:
v = []
for o in offs:
a_ = BK[nm][o][BK[nm][o].index.year == y]
c_ = CTRL[nm][o][CTRL[nm][o].index.year == y]
if len(a_) > 20:
v.append(sh(a_) - sh(c_))
cells.append(float(np.median(v)) if v else float("nan"))
npos = int(np.nansum(np.asarray(cells) > 0))
print(f" {nm:<28}" + "".join(f"{c:>+8.2f}" for c in cells) +
f" anni positivi {npos}/{len(yrs)}")
print()
for nm in top:
Lb = (leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
if nm in BVT else None)
szs = np.concatenate([sizes_of(nm, EX[can][a], SZ, VTL, ACT, Lb) for a in ASSETS])
dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS]))
s = pd.Series(szs, index=dys)
med = s.groupby(s.index.year).median()
rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan")
print(f" size mediana per anno — {nm:<22}" +
"".join(f"{med.get(y, float('nan')):>8.2f}" for y in yrs) +
f" corr col tempo {rho:+.2f}")
print(" ⚠️ una size che DERIVA col tempo non e' controllo del rischio: e' un tilt temporale")
print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il")
print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.")
# noqa
print("\n" + "-" * 112)
print(" 10. NULL DI PERMUTAZIONE DELLA SIZE — la stessa MULTINSIEME di size, riassegnata a")
print(" caso ai trade. Se il vantaggio a iso-peso sopravvive al null, conta QUALE trade")
print(" riceve quale size (informazione); se no, e' solo la DISTRIBUZIONE delle size.")
print("-" * 112)
rng = np.random.default_rng(20260822)
a10 = list(offs[:: max(1, len(offs) // 3)])[:3]
NDRAW = 150
for nm in dict.fromkeys([best_iso, best_sleeve, best_iv, best_rk]):
real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10]))
sz0 = {o: {a: sizes_of(nm, EX[o][a], SZ, VTL, ACT) for a in ASSETS} for o in a10}
nul = []
for _ in range(NDRAW):
v = []
for o in a10:
szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS}
lg = leg_daily(EX[o], szp)
m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0
v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s)))
nul.append(float(np.median(v)))
nul = np.asarray(nul)
pc = float((nul < real).mean()) * 100
print(f" {nm:<26} reale {real:+.3f} null mediana {np.median(nul):+.3f} "
f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} "
f"-> percentile {pc:.1f} (p ~ {(1 - pc / 100):.3f})")
print(f" {NDRAW} permutazioni x {len(a10)} ancore {a10}; seme fisso 20260822.")
print("\n" + "=" * 112)
print(f" fatto in {time.time()-t0:.0f}s")
print("=" * 112)
def netted_target(asset: str, ex: dict, nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1h) -> np.ndarray:
"""Peso NETTO per asset sul grid 1h: 0.75*target TP01 + 0.25*posizione SKH (dir*size), poi
l'eventuale leva di libro sulla gamba TP01. E' cio' che il cron manda a Deribit come UNA
posizione netta sullo stesso strumento."""
tp = np.asarray(TO.hourly_target(asset, 0), float)
ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000
pos = np.zeros(len(ts1))
L1h = np.ones(len(ts1))
if nm is not None and nm in BVT: # BOOKVT: leva anche sulla gamba TP01
L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
sizes = sizes_of(nm, ex, SZ, VTL, ACT, L)
idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True))
L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values
else:
sizes = sizes_of(nm, ex, SZ, VTL, ACT)
t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]]
for k in range(len(t_in)):
j0 = int(np.searchsorted(ts1, t_in[k], side="left"))
j1 = int(np.searchsorted(ts1, t_out[k], side="left"))
pos[j0:j1] = ex["dir"][k] * sizes[k]
return np.clip(W_TP * tp * L1h + W_SKH * pos, -CAP_ASSET, CAP_ASSET)
if __name__ == "__main__":
main()
+645
View File
@@ -0,0 +1,645 @@
#!/usr/bin/env python
"""r0822_volsize_skeptic.py — SCETTICO del filone VOL-SIZE (r0822_vol_size.py).
BERSAGLIO. Un altro agente ha trovato che dare a SKH01 una SIZE PER-TRADE (cella congelata
`VTL tv20 w90`: size fissata all'ingresso = clip(0.20 / rv90(t-1), 0, 3)) vale **+0,070 di
Sharpe di libro a ISO-PESO**, positivo in 23/23 ancore e 8/8 anni. Ha dichiarato lui stesso che
il meccanismo non e' quello che sembra: il guadagno viene dalla FREQUENZA dei trade (+0,051 sul
solo filtro di attivita') e non dalla MAGNITUDINE (+0,003) -> non e' controllo del rischio, e'
un filtro di frequenza. E ha PRE-REGISTRATO senza eseguirlo il test che lo falsificherebbe.
QUATTRO ATTACCHI, dichiarati prima di misurare, con l'esito che mi aspetto:
A1 IL TEST PRE-REGISTRATO — famiglia IV (inverse-vol per-trade sulla vol dell'ASSET) applicata
a SKH01_V1. La ragione dichiarata della famiglia IV e' la GEOMETRIA DELLO STOP: su V2-DD lo
stop e' una percentuale FISSA (4% long / 2% short), quindi il rischio in unita' di sigma
varia col regime; su V1 lo stop e' in ATR, cioe' gia' scalato sulla vol -> l'effetto DEVE
essere MINORE. Previsione direzionale: |eff. su V1| < |eff. su V2-DD|, cella per cella.
Se e' UGUALE, la spiegazione e' sbagliata anche se il numero regge.
ATTESA MIA: che il test sia SENZA POTENZA, perche' sospetto che IV non faccia niente
nemmeno su V2-DD (l'agente ha promosso VTL, non IV). Un test pre-registrato su una
famiglia inerte non falsifica nulla.
A2 LA CONTABILITA' — l'agente dichiara di aver trovato e riparato un look-ahead di
contabilita': "tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA, fino a
3,83 giorni dopo l'ingresso", costo "+0,04 di Sharpe hold-out fantasma", e ne ha tratto
una REGOLA pubblicata. Il compito mi chiede di verificare la riparazione troncando i dati
alla barra d'ingresso. Lo faccio, ma prima verifico la PREMESSA: quale giorno porta davvero
il P&L. ATTESA MIA: la riparazione e' corretta e ridondante.
A3 L'ATTACCO ALLA SPIEGAZIONE — se il guadagno e' un filtro di frequenza, deve essere
riproducibile SENZA ALCUN vol-target. Controllo piu' brutale possibile: size BINARIA 0/1
sulla stessa soglia della regola congelata (L>=1 <=> rv90<=20%), piu' un filtro che scarta
i trade nei periodi di maggiore ATTIVITA' della gamba. Se un binario replica il +0,070,
la formula di sizing e' ORNAMENTO. Controllo di segno obbligatorio: il binario INVERSO
(tieni solo i trade in alta vol) deve DANNEGGIARE, o non sto misurando un filtro.
A4 L'ASSE DELLA FINESTRA — l'agente dichiara che w e' un PICCO, non un plateau (w30 negativo,
w180 ~0, w90 ottimo). Un picco su un asse continuo con 23 ancore e' la firma classica di
un max-of-k. Misuro l'asse fitto (9 finestre) e rifaccio la scelta di w IN-SAMPLE-ONLY.
DISCIPLINA. Non ricalcolo niente che esista: importo `r0822_vol_size` (extract / equity_daily /
leg_daily / size_iv / leverage_series / sizes_from_L / book) e replico BIT-EXACT contro
`sleeves._skyhook_returns()` prima di ogni delta. Il confronto e' sempre ISO-PESO (baseline
ri-scalato allo stesso peso effettivo di SKH), perche' "alzare SKH01" e' gia' stato misurato e
respinto il 26/07. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE per ancora (`anchor_luck_delta`),
mai differenza delle mediane.
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_volsize_skeptic.py [--every K]
--every 8 -> 3 ancore (pilota). default: tutte e 23.
"""
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(_p))
import altlib as A # noqa: E402
import r0702_anchor_skh01 as R # noqa: E402
import r0822_vol_size as V # noqa: E402 (bersaglio: riusato, non riscritto)
from src.strategies.skyhook import SKH01_V1 # noqa: E402
ASSETS = V.ASSETS
OFFSETS_FULL = V.OFFSETS_FULL
FROZEN = dict(tv=0.20, w=90, cap=3.0, active_only=False) # la cella congelata del LEAD
sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr
hold, ins, book = V.hold, V.ins, V.book
# --------------------------------------------------------------------------- famiglie di size
def _keep_to_sizes(ex: dict, keep: pd.Series) -> np.ndarray:
return V.sizes_from_L(ex, keep.astype(float))
def size_vtl(sp: dict):
def f(ex):
return V.sizes_from_L(ex, V.leverage_series(ex["base_daily"], **sp))
return f
def size_bin(sp: dict, mode: str):
"""Size BINARIA 0/1 dalla STESSA L della regola congelata. Nessuna magnitudine: o il trade
si fa a size 1, o non si fa. `ge1` usa la soglia della regola stessa (L>=1 <=> rv90<=tv);
`gemed` la mediana espandente causale di L; `lt1` e' l'INVERSO (controllo di segno)."""
def f(ex):
L = V.leverage_series(ex["base_daily"], **sp)
if mode == "ge1":
keep = pd.Series(L.values >= 1.0, index=L.index)
elif mode == "lt1":
keep = pd.Series(L.values < 1.0, index=L.index)
elif mode == "gemed":
med = L.expanding(min_periods=V.VT_WARM).median()
keep = pd.Series(np.where(med.notna().values, L.values >= med.values, True),
index=L.index)
else:
raise ValueError(mode)
return _keep_to_sizes(ex, keep)
return f
def size_bin_grad(sp: dict):
"""BINARIO + GRADUAZIONE: fra i trade tenuti, la size continua di L. Isola quanto aggiunge
la FORMULA una volta che la SELEZIONE e' gia' stata fatta."""
def f(ex):
L = V.leverage_series(ex["base_daily"], **sp)
g = pd.Series(np.where(L.values >= 1.0, L.values, 0.0), index=L.index)
return V.sizes_from_L(ex, g)
return f
def size_chan(mode: str, binary: bool, w: int = 90, cap: float = 3.0):
"""I DUE CANALI dentro `rv` di una serie all'88% di zeri, isolati come nel bersaglio:
mode='mag' -> vol calcolata sulle sole barre ATTIVE (MAGNITUDINE degli esiti)
mode='freq' -> conteggio di barre attive nella finestra (FREQUENZA dei trade)
binary=False riproduce le celle continue del bersaglio (`VTL neutro attive` / `ACT`);
binary=True le riduce a 0/1 alla mediana ESPANDENTE causale della propria L."""
def f(ex):
if mode == "mag":
Ls = V.leverage_series(ex["base_daily"], tv=None, w=w, cap=cap, active_only=True)
else:
Ls = V.activity_L(ex["base_daily"], w=w, cap=cap)
if not binary:
return V.sizes_from_L(ex, Ls)
med = Ls.expanding(min_periods=V.VT_WARM).median()
keep = pd.Series(np.where(med.notna().values, Ls.values >= med.values, True),
index=Ls.index)
return _keep_to_sizes(ex, keep)
return f
def size_dropact(w: int = 90, q: float = 0.75):
"""Scarta i trade il cui giorno d'ingresso cade nei periodi in cui la gamba e' stata piu'
ATTIVA (conteggio di barre attive negli ultimi w giorni sopra il quantile ESPANDENTE causale
q). Nessun vol-target, nessuna magnitudine, nessun prezzo: solo la frequenza dei trade."""
def f(ex):
base = ex["base_daily"]
cnt = (base != 0.0).astype(float).rolling(w, min_periods=max(5, w // 3)).sum().shift(1)
thr = cnt.expanding(min_periods=V.VT_WARM).quantile(q)
keep = pd.Series(np.where(thr.notna().values & cnt.notna().values,
cnt.values <= thr.values, True), index=base.index)
return _keep_to_sizes(ex, keep)
return f
def iv_family() -> dict:
d = {}
for p in (0.5, 1.0):
for w in (30, 90):
for cp in (2.0, 4.0):
d[f"IV p{p} w{w} cap{cp:.0f}"] = V.size_iv(p, w, cp)
return d
# --------------------------------------------------------------------------- motore
def legs(EX: dict, funcs: dict) -> dict:
"""name -> serie giornaliera della gamba SKH 50/50, per un'ancora."""
out = {"BASE": V.leg_daily(EX, {a: V.size_flat(EX[a]) for a in ASSETS})}
for nm, fn in funcs.items():
out[nm] = V.leg_daily(EX, {a: fn(EX[a]) for a in ASSETS})
return out
def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series):
"""Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol
di gamba -> stesso peso effettivo di SKH). E' la convenzione del bersaglio, riusata."""
vb = vol(leg_base)
m_s = vol(leg) / vb if vb > 0 else 1.0
return book(TP, leg), book(TP, leg_base * m_s), m_s
def delta_table(BK: dict, CT: dict, names, offs, label: str, extra_hold=True) -> dict:
res = {}
print(f" {'cella':<26}{'ISO dShFULL':>12}{'pos/n':>8}{'banda':>20}"
+ (f"{'ISO dShHOLD':>13}{'pos/n':>8}" if extra_hold else "") + f"{'wSKHeff':>9}")
for nm in names:
dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CT[n][o], offs,
metric=sh)
row = dict(dF=dF["median_paired"], nF=dF["n_positive"], lo=dF["lo"], hi=dF["hi"])
s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}"
f" [{row['lo']:+.3f},{row['hi']:+.3f}]".ljust(20 + 26 + 12 + 8))
s = (f" {nm:<26}{row['dF']:>+12.3f}{row['nF']:>5}/{len(offs):<3}"
f"{'[%+.3f,%+.3f]' % (row['lo'], row['hi']):>20}")
if extra_hold:
dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]),
lambda o, n=nm: hold(CT[n][o]), offs, metric=sh)
row.update(dH=dH["median_paired"], nH=dH["n_positive"])
s += f"{row['dH']:>+13.3f}{row['nH']:>5}/{len(offs):<3}"
we = float(np.median([0.25 * m for m in MSCALE[nm]])) if nm in MSCALE else float("nan")
row["weff"] = we
s += f"{we:>9.3f}"
print(s)
res[nm] = row
return res
MSCALE: dict = {}
def per_year(BK: dict, CT: dict, nm: str, offs) -> tuple:
yrs = sorted({int(y) for y in BK[nm][offs[0]].index.year})
cells = []
for y in yrs:
v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y])
for o in offs if (BK[nm][o].index.year == y).sum() > 20]
cells.append(float(np.median(v)) if v else float("nan"))
return yrs, cells
# =========================================================================== main
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--every", type=int, default=1)
args = ap.parse_args()
offs = OFFSETS_FULL[:: max(1, args.every)]
t0 = time.time()
L = 112
print("=" * L)
print(" r0822 VOL-SIZE — SCETTICO. Bersaglio: `VTL tv20 w90` = +0,070 ISO dShFULL (23/23)")
print("=" * L)
print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 {list(offs)}")
print(" lente: path CANONICO (identica nelle due braccia di ogni confronto). TP01 canonico.")
TP = A.tp01_baseline_daily()
# ------------------------------------------------------------------ celle
IV = iv_family()
FUN_V2 = dict(IV)
FUN_V2["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN)
FUN_V2["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1")
FUN_V2["BIN L>=med (0/1)"] = size_bin(FROZEN, "gemed")
FUN_V2["BIN L<1 (INVERSO)"] = size_bin(FROZEN, "lt1")
FUN_V2["BIN+GRAD"] = size_bin_grad(FROZEN)
FUN_V2["DROP attivita' >p75"] = size_dropact(90, 0.75)
FUN_V2["DROP attivita' >p50"] = size_dropact(90, 0.50)
FUN_V2["canale MAGNITUDINE cont."] = size_chan("mag", False)
FUN_V2["canale FREQUENZA cont."] = size_chan("freq", False)
FUN_V2["canale MAGNIT. 0/1"] = size_chan("mag", True)
FUN_V2["canale FREQ. 0/1"] = size_chan("freq", True)
WGRID = (15, 20, 30, 45, 60, 90, 120, 180, 270)
for w in WGRID:
if w != 90:
FUN_V2[f"VTL tv20 w{w}"] = size_vtl(dict(FROZEN, w=w))
FUN_V1 = dict(IV)
FUN_V1["VTL tv20 w90 (LEAD)"] = size_vtl(FROZEN)
FUN_V1["BIN L>=1 (0/1)"] = size_bin(FROZEN, "ge1")
NTRIAL = len(FUN_V2) + len(FUN_V1) + 2 + 4 # +2 baseline, +4 controlli 'L combinata' (§1c)
print(f"\n GRIGLIA DICHIARATA (contata al RIALZO): {len(FUN_V2)} celle su V2-DD + "
f"{len(FUN_V1)} su V1 + 4 controlli 'L combinata' (§1c) + 2 baseline = {NTRIAL} "
f"valutazioni x {len(offs)} ancore. Nessuna cella scelta")
print(" dopo aver guardato: IV e' la famiglia PRE-REGISTRATA, VTL/w e' l'asse dichiarato")
print(" fragile dall'agente, BIN/DROP sono controlli miei dichiarati sopra.")
# ------------------------------------------------------------------ ciclo ancore
BK2, CT2, BK1, CT1 = {}, {}, {}, {}
EX0 = None
print("\n calcolo per ancora ...", flush=True)
for o in offs:
EXV2 = {a: V.extract(a, o) for a in ASSETS}
EXV1 = {a: V.extract(a, o, SKH01_V1) for a in ASSETS}
if o == offs[0]:
EX0 = (EXV2, EXV1)
for tag, EXX, FUN, BKd, CTd in (("V2", EXV2, FUN_V2, BK2, CT2),
("V1", EXV1, FUN_V1, BK1, CT1)):
lg = legs(EXX, FUN)
base = lg["BASE"]
if tag == "V2":
# controlli di §1(c): L calcolata sulla gamba COMBINATA (cio' che fa il ramo
# NAIVE del bersaglio), applicata nei DUE modi. Se coincidono, il "fantasma"
# non e' un problema di QUANDO si applica la size.
for w in (30, 90):
Lc = V.leverage_series(base, **dict(FROZEN, w=w))
lg[f"NAIVEcomb w{w}"] = base * Lc.reindex(base.index).fillna(1.0)
lg[f"COMBingr w{w}"] = V.leg_daily(
EXX, {a: V.sizes_from_L(EXX[a], Lc) for a in ASSETS})
for nm, s in lg.items():
b, c, m_s = iso_pair(TP, s, base)
BKd.setdefault(nm, {})[o] = b
CTd.setdefault(nm, {})[o] = c
if tag == "V2":
MSCALE.setdefault(nm, []).append(m_s)
if o != offs[0]:
del EXV2, EXV1
print(f" fatto in {time.time()-t0:.0f}s")
EXV2, EXV1 = EX0
# ------------------------------------------------------------------ §0 SANITY
print("\n" + "-" * L)
print(" 0. SANITY — replica BIT-EXACT prima di qualunque delta (se non e' 0, il problema")
print(" e' mio e va detto)")
print("-" * L)
from src.portfolio.sleeves import _skyhook_returns
mine = V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS})
off = _skyhook_returns()
d0 = float(np.max(np.abs(mine.values - off.values))) if len(mine) == len(off) else float("nan")
print(f" gamba 50/50 off{offs[0]} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} "
f"({len(mine)} giorni)")
assert d0 < 1e-15, "replica NON bit-exact"
for a in ASSETS:
ref = R.run_asset(a, offs[0])[0]
R._CACHE.clear()
dd = float(np.max(np.abs(V.equity_daily(EXV2[a], V.size_flat(EXV2[a])).values - ref.values)))
print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}")
assert dd < 1e-15
lead = "VTL tv20 w90 (LEAD)"
dLEAD = A.anchor_luck_delta(lambda o: BK2[lead][o], lambda o: CT2[lead][o], offs, metric=sh)
dLEADH = A.anchor_luck_delta(lambda o: hold(BK2[lead][o]), lambda o: hold(CT2[lead][o]),
offs, metric=sh)
print(f" RIPRODUZIONE DEL TITOLO: ISO dShFULL {dLEAD['median_paired']:+.3f} "
f"({dLEAD['n_positive']}/{len(offs)}) ISO dShHOLD {dLEADH['median_paired']:+.3f} "
f"({dLEADH['n_positive']}/{len(offs)}) [pubblicato +0,070 (23/23) / +0,101 (19/23)]")
med_F = float(np.median([sh(BK2["BASE"][o]) for o in offs]))
med_H = float(np.median([sh(hold(BK2["BASE"][o])) for o in offs]))
med_D = float(np.median([maxdd(BK2["BASE"][o]) for o in offs]))
print(f" libro BASE (V2-DD), mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} "
f"/ maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]")
# ------------------------------------------------------------------ §1 CONTABILITA'
print("\n" + "-" * L)
print(" 1. LA CONTABILITA' — verifica della PREMESSA prima della riparazione")
print(" (l'agente: 'tutto il P&L di un trade e' contabilizzato il giorno di CHIUSURA')")
print("-" * L)
tot_ent = tot_ex = tot_chk = 0
for a in ASSETS:
ex = EXV2[a]
d_e = ex["idx"][ex["i_ent"]].floor("D")
d_x = ex["idx"][ex["i_ex"]].floor("D")
dl = ex["base_daily"]
multi = np.where(d_e.values != d_x.values)[0]
ne = nx = ck = 0
for k in multi:
n = float(ex["net"][k])
if abs(n) < 1e-6:
continue
ck += 1
ne += int(abs(float(dl.get(d_e[k], 0.0)) - n) < 1e-9)
nx += int(abs(float(dl.get(d_x[k], 0.0)) - n) < 1e-9)
print(f" {a}: {ck} trade multi-giorno -> ritorno giornaliero == net del trade "
f"al giorno d'INGRESSO {ne}/{ck}, al giorno di USCITA {nx}/{ck}")
tot_ent += ne; tot_ex += nx; tot_chk += ck
print(f" TOTALE: INGRESSO {tot_ent}/{tot_chk} ({tot_ent/max(tot_chk,1)*100:.1f}%) "
f"USCITA {tot_ex}/{tot_chk} ({tot_ex/max(tot_chk,1)*100:.1f}%)")
print(" -> `backtest_signals` fa `equity[i_ent:i_ex+1] = capital`: il gradino comincia")
print(" alla barra d'INGRESSO e il resample giornaliero (last) lo legge quel giorno.")
print("\n (a) TRONCAMENTO ALLA BARRA D'INGRESSO — le size della regola congelata "
"ricalcolate")
print(" con i soli dati <= barra d'ingresso devono coincidere ESATTAMENTE")
worst = 0.0
ntest = 0
for a in ASSETS:
ex = EXV2[a]
full = size_vtl(FROZEN)(ex)
sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= V.SIG_WARM]
sel = sel[:: max(1, len(sel) // 25)][:25]
bad = 0
for k in sel:
i = int(ex["i_ent"][k])
# ricostruisco la gamba usando SOLO i trade gia' CHIUSI prima dell'ingresso k
sub = {kk: ex[kk] for kk in ("n", "idx")}
m = ex["i_ex"] < i
sub = dict(n=i + 1, idx=ex["idx"][: i + 1],
i_ent=ex["i_ent"][m], i_ex=ex["i_ex"][m], net=ex["net"][m])
if not len(sub["i_ent"]):
continue
bd = V.equity_daily(sub, np.ones(len(sub["i_ent"])))
Lt = V.leverage_series(bd, **FROZEN)
day = ex["idx"][i].floor("D")
v = Lt.reindex([day]).values[0]
v = 1.0 if not np.isfinite(v) else float(v)
worst = max(worst, abs(v - float(full[k])))
bad += int(abs(v - float(full[k])) > 1e-9)
ntest += 1
print(f" {a}: {len(sel)-bad}/{len(sel)} size identiche a dati troncati "
f"(divergenze {bad})")
print(f" max|dif| su {ntest} size ricalcolate da zero: {worst:.2e} -> "
f"{'CAUSALE' if worst < 1e-9 else 'NON CAUSALE'}")
print("\n (b) LA 'RIPARAZIONE' E' UN NO-OP: size-all'INGRESSO vs moltiplicare la SERIE")
print(" GIORNALIERA, a parita' di tutto il resto (stessa L, PER ASSET)")
for a in ASSETS:
ex = EXV2[a]
Ls = V.leverage_series(ex["base_daily"], **FROZEN)
s_in = V.equity_daily(ex, V.sizes_from_L(ex, Ls))
s_mul = ex["base_daily"] * Ls.reindex(ex["base_daily"].index).fillna(1.0)
j = pd.concat({"a": s_in, "b": s_mul}, axis=1, join="inner").dropna()
print(f" {a}: max|dif| = {np.max(np.abs(j['a']-j['b'])):.2e} "
f"Sh ingresso {sh(s_in):.4f} vs Sh moltiplicazione {sh(s_mul):.4f}")
print("\n (c) ALLORA COS'E' IL '+0,04 FANTASMA'? Il ramo NAIVE del bersaglio calcola L")
print(" sulla gamba 50/50 COMBINATA e MOLTIPLICA la serie; il ramo 'causale' calcola L")
print(" PER ASSET e la applica all'INGRESSO. Sono DUE differenze in una. Le separo:")
for w in (30, 90):
d = []
for o in offs:
j = pd.concat({"a": BK2[f"NAIVEcomb w{w}"][o], "b": BK2[f"COMBingr w{w}"][o]},
axis=1, join="inner").dropna()
d.append(float(np.max(np.abs(j["a"] - j["b"]))))
print(f" w{w}: [L combinata, MOLTIPLICA] vs [L combinata, size all'INGRESSO] -> "
f"max|dif| mediano {np.median(d):.2e} = IDENTICHE")
print(f"\n {'coppia':<38}{'dShFULL':>10}{'dShHOLD':>10} (delta GREZZI vs baseline nudo,")
print(f" {'':<38}{'':>10}{'':>10} la convenzione del §4 del bersaglio)")
for w in (30, 90):
for nm in (f"NAIVEcomb w{w}", f"COMBingr w{w}",
(lead if w == 90 else f"VTL tv20 w{w}")):
gF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o: BK2["BASE"][o], offs,
metric=sh)
gH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]),
lambda o: hold(BK2["BASE"][o]), offs, metric=sh)
print(f" {nm:<38}{gF['median_paired']:>+10.3f}{gH['median_paired']:>+10.3f}")
print(" -> il 'fantasma' e' interamente [L combinata] - [L per asset]: una scelta di")
print(" SERIE DI RIFERIMENTO, non un look-ahead. Il ramo NAIVE non e' non-causale.")
# ------------------------------------------------------------------ §2 TEST PRE-REGISTRATO
print("\n" + "-" * L)
print(" 2. IL TEST PRE-REGISTRATO — famiglia IV su V2-DD (stop % FISSO) vs V1 (stop in ATR)")
print(" Previsione dichiarata dall'agente: |effetto su V1| < |effetto su V2-DD|.")
print("-" * L)
n2 = {a: len(EXV2[a]["i_ent"]) for a in ASSETS}
n1 = {a: len(EXV1[a]["i_ent"]) for a in ASSETS}
print(f" trade all'ancora {offs[0]}: V2-DD {n2} V1 {n1}")
mb2 = float(np.median([sh(BK2["BASE"][o]) for o in offs]))
mb1 = float(np.median([sh(BK1["BASE"][o]) for o in offs]))
print(f" libro BASE mediana di banda: V2-DD ShFULL {mb2:.3f} | V1 ShFULL {mb1:.3f}")
names_iv = list(IV)
print(f"\n {'cella IV':<22}{'V2-DD ISO dF':>14}{'pos/n':>8}{'V1 ISO dF':>12}{'pos/n':>8}"
f"{'|V1|<|V2|?':>12}")
ok = tot = 0
e2s, e1s = [], []
for nm in names_iv:
a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs,
metric=sh)
a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs,
metric=sh)
e2, e1 = a2["median_paired"], a1["median_paired"]
e2s.append(e2); e1s.append(e1)
good = abs(e1) < abs(e2)
ok += int(good); tot += 1
print(f" {nm:<22}{e2:>+14.3f}{a2['n_positive']:>5}/{len(offs):<3}{e1:>+12.3f}"
f"{a1['n_positive']:>5}/{len(offs):<3}{('SI' if good else 'NO'):>12}")
print(f"\n previsione verificata in {ok}/{tot} celle della famiglia")
print(f" |effetto| mediano di famiglia: V2-DD {np.median(np.abs(e2s)):.3f} "
f"V1 {np.median(np.abs(e1s)):.3f}")
print(f" effetto mediano SEGNATO: V2-DD {np.median(e2s):+.3f} V1 {np.median(e1s):+.3f}")
print(" ⚠️ POTENZA DEL TEST: se |effetto su V2-DD| e' gia' ~0, la previsione e' verificabile")
print(" solo per caso e il gate pre-registrato NON puo' falsificare niente.")
for nm in ("VTL tv20 w90 (LEAD)", "BIN L>=1 (0/1)"):
a2 = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs,
metric=sh)
a1 = A.anchor_luck_delta(lambda o, n=nm: BK1[n][o], lambda o, n=nm: CT1[n][o], offs,
metric=sh)
print(f" (riferimento) {nm:<24} V2-DD {a2['median_paired']:+.3f} "
f"({a2['n_positive']}/{len(offs)}) V1 {a1['median_paired']:+.3f} "
f"({a1['n_positive']}/{len(offs)})")
# ------------------------------------------------------------------ §3 ATTACCO ALLA SPIEGAZIONE
print("\n" + "-" * L)
print(" 3. ATTACCO ALLA SPIEGAZIONE — quanto del +0,070 resta senza NESSUN vol-target?")
print(" Tutte ISO-PESO vs lo stesso baseline. 'wSKHeff' = peso effettivo di SKH nel libro")
print("-" * L)
grp = [lead, "BIN L>=1 (0/1)", "BIN+GRAD", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)",
"DROP attivita' >p75", "DROP attivita' >p50",
"canale MAGNITUDINE cont.", "canale FREQUENZA cont.",
"canale MAGNIT. 0/1", "canale FREQ. 0/1"]
RES = delta_table(BK2, CT2, grp, offs, "attacco")
base_eff = RES[lead]["dF"]
print()
for nm in grp[1:]:
frac = RES[nm]["dF"] / base_eff * 100 if abs(base_eff) > 1e-9 else float("nan")
print(f" {nm:<26} recupera {frac:>6.0f}% del vantaggio del LEAD")
# quanti trade tocca ciascun filtro
print(f"\n {'cella':<26}{'trade tenuti':>14}{'su':>6}{'size mediana':>14}"
f"{'size = 0':>10}")
for nm in [lead] + grp[1:]:
kept = tot_t = zer = 0
med = []
for a in ASSETS:
s = FUN_V2[nm](EXV2[a])
tot_t += len(s); kept += int((s > 0).sum()); zer += int((s == 0).sum())
med.append(s)
mm = float(np.median(np.concatenate(med)))
print(f" {nm:<26}{kept:>14}{tot_t:>6}{mm:>14.2f}{zer:>10}")
print(f"\n COSA SELEZIONA il filtro (ancora {offs[0]}, i due asset uniti): net medio dei")
print(" trade TENUTI vs SCARTATI, e attivita' trailing media dei due gruppi")
print(f" {'cella':<26}{'net TENUTI':>12}{'net SCARTATI':>14}{'att.TEN':>10}{'att.SCA':>10}"
f"{'%tenuti':>9}")
for nm in ("BIN L>=1 (0/1)", "BIN L>=med (0/1)", "BIN L<1 (INVERSO)",
"DROP attivita' >p50", "canale MAGNIT. 0/1", "canale FREQ. 0/1"):
nk, nd, ak, ad = [], [], [], []
for a in ASSETS:
ex = EXV2[a]
szv = FUN_V2[nm](ex)
cnt = ((ex["base_daily"] != 0.0).astype(float)
.rolling(90, min_periods=30).sum().shift(1))
av = cnt.reindex(ex["day_ent"]).values
keep = szv > 0
nk += list(ex["net"][keep]); nd += list(ex["net"][~keep])
ak += list(av[keep]); ad += list(av[~keep])
f_ = 100.0 * len(nk) / max(len(nk) + len(nd), 1)
print(f" {nm:<26}{np.mean(nk)*100:>11.2f}%{np.mean(nd)*100 if nd else float('nan'):>13.2f}%"
f"{np.nanmean(ak):>10.1f}{np.nanmean(ad) if ad else float('nan'):>10.1f}{f_:>9.0f}")
print("\n per anno (ISO dSh, mediana appaiata sulle ancore):")
yrs = None
for nm in [lead, "BIN L>=med (0/1)", "canale FREQUENZA cont.", "BIN L<1 (INVERSO)"]:
yrs, cells = per_year(BK2, CT2, nm, offs)
if nm == lead:
print(" " + f"{'':<26}" + "".join(f"{y:>8}" for y in yrs))
npos = int(np.nansum(np.asarray(cells) > 0))
print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cells) +
f" anni positivi {npos}/{len(yrs)}")
# ------------------------------------------------------------------ §4 ASSE DELLA FINESTRA
print("\n" + "-" * L)
print(" 4. L'ASSE DELLA FINESTRA — picco o plateau? + scelta di w IN-SAMPLE-ONLY")
print("-" * L)
wnames = [(w, f"VTL tv20 w{w}" if w != 90 else lead) for w in WGRID]
bd0 = EXV2["BTC"]["base_daily"]
print(f" {'w':>6}{'ISO dShFULL':>14}{'pos/n':>8}{'ISO dShIS':>12}{'ISO dShHOLD':>13}"
f"{'pos/n':>8}{'barre att.':>12}")
wf, wi, wh = {}, {}, {}
for w, nm in wnames:
dF = A.anchor_luck_delta(lambda o, n=nm: BK2[n][o], lambda o, n=nm: CT2[n][o], offs,
metric=sh)
dI = A.anchor_luck_delta(lambda o, n=nm: ins(BK2[n][o]), lambda o, n=nm: ins(CT2[n][o]),
offs, metric=sh)
dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK2[n][o]), lambda o, n=nm: hold(CT2[n][o]),
offs, metric=sh)
wf[w], wi[w], wh[w] = dF["median_paired"], dI["median_paired"], dH["median_paired"]
nact = float(np.median((bd0 != 0.0).astype(float).rolling(w).sum().dropna().values))
print(f" {w:>6}{wf[w]:>+14.3f}{dF['n_positive']:>5}/{len(offs):<3}{wi[w]:>+12.3f}"
f"{wh[w]:>+13.3f}{dH['n_positive']:>5}/{len(offs):<3}{nact:>12.0f}")
wbest_full = max(WGRID, key=lambda w: wf[w])
wbest_is = max(WGRID, key=lambda w: wi[w])
print(f"\n argmax su FULL (cio' che l'agente ha pubblicato): w = {wbest_full} "
f"({wf[wbest_full]:+.3f})")
print(f" argmax IN-SAMPLE-ONLY (la sola scelta ammessa): w = {wbest_is} "
f"(IS {wi[wbest_is]:+.3f}) -> quella cella vale FULL {wf[wbest_is]:+.3f} / "
f"HOLD {wh[wbest_is]:+.3f}")
order = sorted(WGRID)
k = order.index(wbest_full)
nb = [order[k - 1] if k > 0 else None, order[k + 1] if k + 1 < len(order) else None]
for w in nb:
if w is not None:
print(f" vicino w={w:<4} vale {wf[w]:+.3f} = {wf[w]/wf[wbest_full]*100:>5.0f}% "
f"del picco")
pos = sum(1 for w in WGRID if wf[w] > 0)
print(f" celle con ISO dShFULL > 0: {pos}/{len(WGRID)} "
f"spread della famiglia {max(wf.values())-min(wf.values()):.3f} "
f"sd fra celle {np.std(list(wf.values()), ddof=1):.3f}")
print(f" stima ONESTA se w e' SCELTO (mediana della famiglia, come per la banda "
f"d'ancora): {np.median(list(wf.values())):+.3f} FULL / "
f"{np.median(list(wh.values())):+.3f} HOLD contro il picco "
f"{wf[wbest_full]:+.3f} / {wh[wbest_full]:+.3f}")
rho = float(np.corrcoef([wi[w] for w in WGRID], [wh[w] for w in WGRID])[0, 1])
print(f" corr(ISO dSh in-sample, ISO dSh hold-out) fra le {len(WGRID)} finestre: {rho:+.2f}")
print(" LETTURA (attacco 4 FALLITO, e il motivo e' informativo): la scelta di w")
print(" in-sample-only cade sulla cella pubblicata, e la corr IS/HOLD e' POSITIVA -> l'asse")
print(" non e' rumore. E non e' nemmeno un PICCO: e' una SOGLIA. Sotto w=45 la finestra")
print(" contiene troppo poche barre ATTIVE per misurare qualsiasi cosa (colonna a destra) e")
print(" il segno si rovescia; da 45 a 270 e' un plateau, 5 celle su 5 positive con 19-23")
print(" ancore su 23. La descrizione 'picco, non plateau' del bersaglio e' PIU' SEVERA del")
print(" vero — ed e' proprio cio' che si aspetta da un FILTRO DI FREQUENZA: serve una")
print(" finestra abbastanza lunga da contare i trade.")
# ------------------------------------------------------------------ §5 sintesi
print("\n" + "-" * L)
print(" 5. GATE (per il contratto di consegna)")
print("-" * L)
can = offs[0]
legs_can = {}
for nm in ("BASE", lead, "BIN L>=med (0/1)"):
legs_can[nm] = (V.leg_daily(EXV2, {a: V.size_flat(EXV2[a]) for a in ASSETS})
if nm == "BASE"
else V.leg_daily(EXV2, {a: FUN_V2[nm](EXV2[a]) for a in ASSETS}))
nlos = int((EXV2["BTC"]["net"] < 0).sum() + (EXV2["ETH"]["net"] < 0).sum())
ntot = int(len(EXV2["BTC"]["net"]) + len(EXV2["ETH"]["net"]))
for nm, s_ in legs_can.items():
m = A.marginal_vs_tp01(s_)
imp = A.implausible_sharpe(s_, n_trades=ntot, n_losing_trades=nlos)
print(f" {nm:<22} marginal {str(m.get('marginal_verdict')):<9} corr {m.get('corr_full')}"
f" implausible={imp['implausible']} (perdite/attive "
f"{imp.get('loss_frac', 0)*100:.1f}%)")
trials = [float(np.median([sh(BK2[n][o]) for o in offs])) for n in BK2]
dsr_c, sr0 = A.deflated_sharpe(float(np.median([sh(BK2[lead][o]) for o in offs])), trials,
BK2[lead][can])
dsr_b, _ = A.deflated_sharpe(med_F, trials, BK2["BASE"][can])
print(f" deflated-Sharpe: candidato {dsr_c:.3f} / BASELINE {dsr_b:.3f} su {len(trials)} "
f"trial (sd {np.std(trials, ddof=1):.4f}) -> VACUO, lo passa anche il baseline")
print(" null de-levering: DEGENERE per costruzione (sh(k*base) == sh(base)); il suo")
print(" sostituto e' il confronto ISO-PESO usato in tutte le tabelle sopra.")
print(" causalita': §1(a) 50/50 size ricalcolate da dati troncati, max|dif| 0.00e+00.")
print("\n" + "-" * L)
print(" 6. SINTESI DEI NUMERI CHE CONTANO")
print("-" * L)
print(f" fortuna d'ancora del LIBRO BASE: ShFULL canonico "
f"{sh(BK2['BASE'][0]) if 0 in BK2['BASE'] else float('nan'):.3f} vs mediana "
f"{med_F:.3f}")
print(f" vantaggio del LEAD (ISO): {dLEAD['median_paired']:+.3f} FULL / "
f"{dLEADH['median_paired']:+.3f} HOLD")
print(f" riprodotto dal FILTRO BINARIO: {RES['BIN L>=1 (0/1)']['dF']:+.3f} FULL / "
f"{RES['BIN L>=1 (0/1)']['dH']:+.3f} HOLD")
print(f" riprodotto dal BINARIO alla MEDIANA:{RES['BIN L>=med (0/1)']['dF']:+.3f} FULL / "
f"{RES['BIN L>=med (0/1)']['dH']:+.3f} HOLD ({RES['BIN L>=med (0/1)']['nF']}/{len(offs)} ancore)")
print(f" controllo di segno (INVERSO): {RES['BIN L<1 (INVERSO)']['dF']:+.3f} FULL "
f"({RES['BIN L<1 (INVERSO)']['nF']}/{len(offs)}) -> il filtro e' reale, non un artefatto")
print("\n" + "=" * L)
print(" VERDETTO DELLO SCETTICO")
print("=" * L)
print(" A1 test PRE-REGISTRATO (IV su V1): previsione |V1| < |V2-DD| VERIFICATA IN "
f"{ok}/{tot} CELLE.")
print(" L'effetto e' PIU' GRANDE dove lo stop e' gia' in ATR (V1 mediana "
f"{np.median(np.abs(e1s)):.3f} vs V2-DD {np.median(np.abs(e2s)):.3f}), e il test NON e'")
print(" senza potenza (su V2-DD la famiglia arriva a +0.067 con 23/23). -> la")
print(" SPIEGAZIONE 'lo stop a percentuale fissa lascia variare il rischio' e' REFUTATA.")
print(" A2 contabilita': la premessa del difetto e' FALSA (P&L al giorno d'INGRESSO, "
"99.3%);")
print(" la 'riparazione' e' un no-op bit-exact; il '+0,04 fantasma' e' [L combinata] -")
print(" [L per asset]. Nessun look-ahead c'era, e la variante scartata era la MIGLIORE.")
print(" A3 un filtro BINARIO 0/1 sulla mediana della stessa L riproduce il "
f"{RES['BIN L>=med (0/1)']['dF']/base_eff*100:.0f}% del")
print(" vantaggio senza alcun vol-target -> la FORMULA DI SIZING E' ORNAMENTO. Il canale")
print(" FREQUENZA da solo fa +0.051, la MAGNITUDINE +0.003 (replica esatta del bersaglio).")
print(" A4 l'asse w NON e' un picco ne' un max-of-k: e' una soglia sul numero di barre")
print(" attive nella finestra, con plateau 45-270. Attacco fallito.")
print(" --> il NUMERO regge (+0.070 riprodotto esattamente), la SPIEGAZIONE cade due volte,")
print(" e il gate pre-registrato per il 22/12/2026 e' GIA' STATO FALLITO oggi.")
print(f"\n fatto in {time.time()-t0:.0f}s")
print("=" * L)
if __name__ == "__main__":
main()
+857
View File
@@ -0,0 +1,857 @@
"""R0822 VRP-QUOTE-VERE — esiste una struttura di reddito in opzioni ESEGUIBILE a $600-3.000
che sopravvive a fee reali, spread reali e lotto minimo?
NON e' "rifare VRP01". La domanda e' se la regola dura del progetto — *niente short-vol da
MODELLO in deploy* (19/06, riconfermata 3 volte) — si possa sostituire con *short-vol da QUOTE*,
che e' un'altra cosa: il 30/07 il premio era ancora prezzato Black-Scholes su DVOL ATM e la
misura sulle quote reali dava f = 0.73 del modello, con il difetto NON nella gamba venduta
(f_short 1.01) ma nell'ALA che si compra (f_long 2.23). Da allora la condizione e' cambiata:
la catena vera la raccogliamo noi, oraria, per strike, con bid/ask.
ORDINE DELLE DOMANDE (deliberato). L'eseguibilita' viene PRIMA del rendimento — regola del
30/07: "prima di misurare il rendimento a un capitale dato, misurare il lotto minimo del venue".
Se non si esegue, il resto e' accademia.
§1 LIMITE DI REGIME — risultato di prima riga, non nota a pie' di pagina
§2 IL CAMPIONE — quante settimane ci sono DAVVERO, e perche'
§3 ESEGUIBILITA' — lotti, famiglie, e su quale delle due si esegue
§4 BACKTEST SU QUOTE REALI — griglia dichiarata, fill al bid/ask, fee di listino
§5 IL RISCHIO STA NEL MINIMO — non nelle chiusure settimanali
§6 GATE — inclusi quelli che NON si possono far girare
§7 VERDETTO
CONVENZIONI CHE DECIDONO IL RISULTATO (dichiarate qui, non sepolte nel codice)
(a) FILL: si vende al BID e si compra all'ASK, sempre. Il mid e' il modello sotto mentite
spoglie e compare solo come diagnostica, mai come cella selezionabile.
(b) FEE: listino Deribit vero — `min(0.03% del sottostante, 12.5% del premio della SINGOLA
opzione)` per gamba, piu' la fee di consegna sulle gambe ITM a scadenza. Lo sleeve usa
invece un forfait del 12.5% del credito NETTO: qui si misura di quanto sbaglia.
(c) SETTLEMENT: indice reale alla scadenza dal feed certificato. Deribit regola sulla media
dell'indice nei 30 minuti prima delle 08:00 UTC; qui si usa la barra 1h di chiusura —
approssimazione dichiarata, non nascosta.
(d) CAPITALE IMPEGNATO: il denominatore e' il rischio massimo della struttura
`(K_short - K_long) x lotto - credito`. E' il limite INFERIORE del capitale richiesto;
quello vero dipende dal regime di margine del conto e NON e' leggibile da dati pubblici.
Riportato anche il limite superiore (cash-secured, la convenzione del sleeve).
RAM: la catena si legge filtrata in LETTURA (solo put, solo le colonne che servono) con
pyarrow.dataset — `bite_archive.parquet` intero esplode in memoria e ucciderebbe gli altri agenti.
nice -n 19 timeout 900 uv run python scripts/research/r0822_vrp_real_quotes.py
... --no-live # salta le letture dal venue (usa la cache se c'e')
"""
from __future__ import annotations
import argparse
import json
import sys
import time
import urllib.request
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"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
import cblib as CB # noqa: E402
CHAIN_DIR = ROOT / "data" / "raw" / "cb_chain"
SNAP_JSONL = ROOT / "data" / "options_daily" / "snapshots.jsonl"
CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822_venue.json")
# --- listino Deribit (fonte: campo `taker_commission` letto dal venue + schema fee pubblico) ---
FEE_UNDERLYING = 0.0003 # 0.03% del sottostante per gamba, per contratto
FEE_PREMIUM_CAP = 0.125 # cap: 12.5% del premio della SINGOLA opzione
FEE_DELIVERY = 0.00015 # 0.015% del sottostante sulle gambe ITM a scadenza
SLEEVE_FEE_FRAC = 0.125 # forfait dello sleeve: 12.5% del credito NETTO
DTE_LO, DTE_HI, DTE_TARGET = 4.0, 10.0, 7.0
WEEKS_PER_YEAR = 52.0
# ---------------------------------------------------------------- griglia DICHIARATA
# Si conta al RIALZO: ogni variante provata e' un trial, anche quelle scartate.
SHORT_DELTAS = (-0.35, -0.28, -0.20, -0.12)
LONG_DELTAS = (-0.20, -0.10, -0.05)
CELLS = [(s, l) for s in SHORT_DELTAS for l in LONG_DELTAS if abs(l) < abs(s)]
FILLS = ("bid_ask", "mid") # `mid` = diagnostica, NON selezionabile
ASSETS = ("BTC", "ETH")
CANON = (-0.28, -0.10) # la cella di VRP01
def hr(t: str = "", ch: str = "=") -> None:
print("\n" + ch * 100)
if t:
print(t)
print(ch * 100)
# ================================================================== dati
def load_puts() -> pd.DataFrame:
"""Solo le PUT, solo le colonne che servono, filtrate in LETTURA."""
import pyarrow.dataset as ds
cols = ["ts", "asset", "instrument_name", "strike", "option_type", "exp",
"bid", "ask", "mid", "iv", "delta", "open_interest", "quote_status"]
d = ds.dataset(str(CHAIN_DIR), format="parquet")
df = d.to_table(columns=cols, filter=(ds.field("option_type") == "P")).to_pandas()
df["ts"] = pd.to_datetime(df["ts"], utc=True).dt.as_unit("ns")
df["exp"] = pd.to_datetime(df["exp"], utc=True)
df["dte"] = (df["exp"] - df["ts"]).dt.total_seconds() / 86400.0
return df.drop_duplicates(subset=["ts", "instrument_name"], keep="last").reset_index(drop=True)
def venue_read(live: bool) -> dict:
"""Specifiche e liquidita' lette DAL VENUE. Un fallimento si dichiara, non si inventa."""
if not live and CACHE.exists():
return json.loads(CACHE.read_text())
def api(path, **kw):
q = "&".join(f"{k}={v}" for k, v in kw.items())
with urllib.request.urlopen(
f"https://www.deribit.com/api/v2/public/{path}?{q}", timeout=25) as r:
return json.load(r)["result"]
out = {"ok": False, "ts": pd.Timestamp.now(tz="UTC").isoformat()}
try:
out["index"] = {c: api("get_index_price", index_name=f"{c}_usd")["index_price"]
for c in ("btc", "eth")}
specs, summ = {}, []
for cur in ("BTC", "ETH", "USDC"):
for i in api("get_instruments", currency=cur, kind="option", expired="false"):
fam = i["instrument_name"].split("-")[0]
if fam not in specs:
specs[fam] = {k: i.get(k) for k in
("min_trade_amount", "contract_size", "settlement_currency",
"quote_currency", "taker_commission", "base_currency")}
summ += api("get_book_summary_by_currency", currency=cur, kind="option")
time.sleep(0.4)
out["specs"] = specs
out["summary"] = [{k: r.get(k) for k in
("instrument_name", "bid_price", "ask_price", "mid_price",
"mark_price", "mark_iv", "open_interest", "volume",
"underlying_price")} for r in summ]
out["ok"] = True
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(json.dumps(out))
except Exception as exc: # noqa: BLE001
out["error"] = f"{type(exc).__name__}: {exc}"
if CACHE.exists():
cached = json.loads(CACHE.read_text())
cached["stale"] = out["error"]
return cached
return out
def specs_from_disk() -> dict:
"""Le specifiche gia' registrate dal progetto (`data/options_daily/snapshots.jsonl`).
Serve come controllo indipendente della lettura live: due percorsi, stesso numero."""
mins, fees, last_ts = {}, {}, {}
if not SNAP_JSONL.exists():
return {}
with SNAP_JSONL.open() as f:
for line in f:
try:
r = json.loads(line)
except Exception: # noqa: BLE001, S112
continue
if r.get("rec") != "chain":
continue
c = r["currency"]
mins.setdefault(c, set()).add(r.get("min_trade_amount"))
fees.setdefault(c, set()).add(r.get("taker_comm"))
last_ts[c] = r["snap_ts"]
return {c: {"min": sorted(x for x in mins[c] if x is not None),
"taker": sorted(x for x in fees[c] if x is not None),
"ts": pd.Timestamp(last_ts[c], unit="ms", tz="UTC")} for c in mins}
# ================================================================== struttura
def leg_fee_usd(prem_base: float, spot: float) -> float:
"""Fee di listino per UNA gamba, per contratto (1 unita' di sottostante), in USD."""
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot)
def entry_snapshots(puts: pd.DataFrame, asset: str, offset_h: float = 0.0) -> dict:
"""Per ogni scadenza settimanale: lo snapshot piu' vicino a (7 giorni + offset) dalla scadenza.
L'offset e' l'ANCORA: entrare alle 08:00 del venerdi' precedente e' UNA delle scelte possibili,
e questo progetto ha misurato 4 volte che l'ancora e' un max-of-k non dichiarato.
"""
p = puts[(puts["asset"] == asset) & (puts["dte"] >= DTE_LO) & (puts["dte"] <= DTE_HI)]
tgt = DTE_TARGET + offset_h / 24.0
out = {}
for exp, g in p.groupby("exp"):
ts_all = np.array(sorted(g["ts"].unique()))
if not len(ts_all):
continue
dte = (exp - pd.DatetimeIndex(ts_all)).total_seconds() / 86400.0
ok = (dte >= DTE_LO) & (dte <= DTE_HI)
if not ok.any():
continue
cand = ts_all[ok][np.argsort(np.abs(dte[ok] - tgt))]
for ts in cand[:6]: # al piu' 6 tentativi per scadenza
snap = g[g["ts"] == ts]
if snap["bid"].notna().sum() >= 2:
out[exp] = (pd.Timestamp(ts), snap)
break
return out
def build_trades(puts: pd.DataFrame, asset: str, short_d: float, long_d: float,
fill: str = "bid_ask", offset_h: float = 0.0,
lot: float = 1.0, today: pd.Timestamp | None = None) -> pd.DataFrame:
"""Un trade per scadenza: ingresso su quote REALI, tenuto a scadenza, regolato sull'indice."""
today = today or pd.Timestamp.now(tz="UTC")
S = CB.spot_series(asset)
V = CB.dvol_series(asset)
rows, rejected = [], {"gambe_mancanti": 0, "credito_non_positivo": 0}
for exp, (ts, snap) in entry_snapshots(puts, asset, offset_h).items():
if exp >= today: # non ancora regolata: non e' un trade
continue
legs = CB.pick_legs(snap, short_d, long_d)
if legs is None:
rejected["gambe_mancanti"] += 1
continue
spot = float(S.asof(ts))
dte = (exp - ts).total_seconds() / 86400.0
dvol = float(V.asof(ts))
hist = V[V.index < ts]
ivr = float((hist < dvol).mean()) if len(hist) else np.nan
if fill == "bid_ask":
ps, pl = legs["bid_short"], legs["ask_long"] # vendi al bid, compra all'ask
else:
ps, pl = legs["mid_short"], legs["mid_long"]
if not (np.isfinite(ps) and np.isfinite(pl)):
rejected["gambe_mancanti"] += 1
continue
cred = (ps - pl) * spot # USD per 1 unita' di sottostante
if cred <= 0:
# premio non monotono nello strike (difetto certificato di `certify_cb_chain`):
# una put piu' OTM quotata sopra una meno OTM. Non e' una struttura, e' una quota rotta.
rejected["credito_non_positivo"] += 1
continue
ST = float(S.asof(exp))
payoff = max(legs["k_short"] - ST, 0.0) - max(legs["k_long"] - ST, 0.0)
fee = leg_fee_usd(ps, spot) + leg_fee_usd(pl, spot)
for K in (legs["k_short"], legs["k_long"]): # consegna sulle gambe ITM
intr = max(K - ST, 0.0)
if intr > 0:
fee += min(FEE_DELIVERY * ST, FEE_PREMIUM_CAP * intr)
width = legs["k_short"] - legs["k_long"]
mod = CB.f_factors(legs, spot, dvol / 100.0, dte)
rows.append(dict(
asset=asset, ts=ts, exp=exp, dte=dte, spot=spot, ST=ST,
inst_short=legs["inst_short"], inst_long=legs["inst_long"],
cred_real=cred, # per 1 unita': lo vuole CB.close_cost_path
k_short=legs["k_short"], k_long=legs["k_long"], width=width,
d_short=legs["d_short"], d_long=legs["d_long"],
cred=cred * lot, payoff=payoff * lot, fee=fee * lot,
pnl=(cred - payoff - fee) * lot,
fee_sleeve=SLEEVE_FEE_FRAC * mod["cred_mod"] * lot,
fee_sleeve_su_reale=SLEEVE_FEE_FRAC * cred * lot,
cred_mod=mod["cred_mod"] * lot,
risk=width * lot - cred * lot,
dvol=dvol, ivrank=ivr, f_net=mod["f_net"],
und_ret=ST / spot - 1.0,
))
df = pd.DataFrame(rows)
if not df.empty:
df = df.sort_values("exp").reset_index(drop=True)
df.attrs["rejected"] = rejected
return df
def stats(tr: pd.DataFrame) -> dict:
"""Metriche di una serie di trade settimanali. Il denominatore e' il RISCHIO impegnato."""
if tr.empty or len(tr) < 2:
return dict(n=len(tr), sharpe=np.nan, mean=np.nan, t=np.nan)
r = (tr["pnl"] / tr["risk"]).to_numpy(float) # ritorno sul capitale a rischio
sd = float(np.std(r, ddof=1))
m = float(np.mean(r))
eq = np.cumprod(1.0 + r)
dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq)))
return dict(n=len(tr), mean=m, sd=sd,
sharpe=(m / sd * np.sqrt(WEEKS_PER_YEAR)) if sd > 0 else np.nan,
t=(m / (sd / np.sqrt(len(r)))) if sd > 0 else np.nan,
maxdd_close=dd, wins=int((tr["pnl"] > 0).sum()),
usd_week=float(tr["pnl"].mean()), usd_tot=float(tr["pnl"].sum()))
def weekly_series(tr: pd.DataFrame) -> pd.Series:
return pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float),
index=pd.DatetimeIndex(tr["exp"])).sort_index()
def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float]:
rng = np.random.default_rng(seed)
if len(x) < 2:
return (np.nan, np.nan)
bs = rng.choice(x, size=(n, len(x)), replace=True).mean(axis=1)
return float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5))
# ================================================================== main
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--no-live", action="store_true", help="non interrogare il venue")
args = ap.parse_args()
print(__doc__.split("\n\n")[0])
puts = load_puts()
today = pd.Timestamp.now(tz="UTC")
venue = venue_read(not args.no_live)
# ------------------------------------------------------------ §1 REGIME
hr("§1 LIMITE DI REGIME — il campione descrive il regime in cui la strategia sta FLAT")
print("Non e' una nota a pie' di pagina: VRP01 entra SOLO se IV-rank > 0.30 (il gate e' l'alpha,\n"
"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)
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} / "
f"max {win.max():.1f}")
print(f" vs storia 2021+ ({len(hist)} giorni): la MEDIANA del campione sta al "
f"{float((hist < win.median()).mean()):.0%} pctl, il suo MASSIMO al "
f"{float((hist < win.max()).mean()):.0%} pctl")
print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita «DVOL max al 24\u00b0/28\u00b0\n"
" pctl» dal campione del 30/07. Un MASSIMO cresce con la finestra — e' una statistica\n"
" di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n"
" e' bassissima.")
# ------------------------------------------------------------ §2 CAMPIONE
hr("§2 IL CAMPIONE — quante settimane ci sono DAVVERO")
pre = puts[puts["ts"] < pd.Timestamp("2026-06-09", tz="UTC")]
print(f" catena su disco: {puts['ts'].min():%Y-%m-%d} → {puts['ts'].max():%Y-%m-%d} "
f"({(puts['ts'].max() - puts['ts'].min()).days} giorni, {len(puts):,} righe put)")
if not pre.empty:
print(f" ma PRIMA del 2026-06-09 l'archivio ereditato seguiva UNA sola scadenza, in "
f"finestra {pre['dte'].min():.0f}-{pre['dte'].max():.0f} DTE:")
print(f" → la finestra settimanale {DTE_LO:.0f}-{DTE_HI:.0f} DTE NON ESISTE prima "
f"della raccolta full-chain.")
print("\n Trade REGOLATI utilizzabili (cella canonica VRP01 δ-0.28/-0.10, fill bid/ask):")
canon: dict[str, pd.DataFrame] = {}
for a in ASSETS:
lot = 1.0
tr = build_trades(puts, a, *CANON, lot=lot, today=today)
canon[a] = tr
rej = tr.attrs.get("rejected", {}) if not tr.empty else {}
print(f" {a}: {len(tr)} settimane regolate ({tr['exp'].min():%Y-%m-%d} → "
f"{tr['exp'].max():%Y-%m-%d}) scartate: {rej}")
print("\n ⚠️ «3,7 mesi ≈ 16 scadenze settimanali» e' FALSO su questi dati: sono 10 per asset.")
print("\n Gate IV-rank>0.30 di VRP01 sulle settimane utilizzabili:")
tot = passed = 0
for a in ASSETS:
tr = canon[a]
n = int((tr["ivrank"] > 0.30).sum())
tot += len(tr)
passed += n
print(f" {a}: {n}/{len(tr)} passano (IV-rank min {tr['ivrank'].min():.3f} / "
f"max {tr['ivrank'].max():.3f})")
print(f" TOTALE: {passed}/{tot}. Il sleeve sarebbe stato FLAT per l'intero campione.")
print("\n QUANDO TORNERA' MISURABILE — il gate non e' chiuso per sempre, e la soglia e' un numero.")
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)
vals = V.to_numpy(float)
ivr = np.full(len(vals), np.nan)
for i in range(1, len(vals)):
ivr[i] = (vals[:i] < vals[i]).mean()
ser = pd.Series(ivr, index=V.index).dropna()
per_year = ser.groupby(ser.index.year).apply(lambda x: float((x > 0.30).mean()))
thr = float(np.quantile(vals[:-1], 0.30))
last_open = ser[ser > 0.30]
since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None
inwin = ser[(ser.index >= puts["ts"].min()) & (ser.index <= puts["ts"].max())]
near = inwin.max() if len(inwin) else float("nan")
print(f" {a}: quota di giorni col gate APERTO per anno — "
+ " ".join(f"{y}:{v:.0%}" for y, v in per_year.items()))
print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} "
f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto "
f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)")
print(f" quanto vicino c'e' andato DENTRO la finestra: IV-rank max "
f"{near:.4f} il {inwin.idxmax():%Y-%m-%d} — "
f"{'mancato per ' + format(0.30 - near, '.4f') if near <= 0.30 else 'aperto'}")
print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n"
" persistente. Ma il margine NON e' comodo: il gate ha sfiorato l'apertura, e\n"
" bastano ~+10% di DVOL per riaprirlo. La misura giusta si potra' rifare quando quel numero sara' superato,\n"
" e da allora servira' comunque un anno di settimane per avere un campione.")
ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS}
print(f"\n E il sottostante nel campione: BTC {ret['BTC']:+.1%}, ETH {ret['ETH']:+.1%} — "
f"vol implicita bassa E rialzo forte\n = il regime IDEALE per vendere put. "
f"Il campione e' il migliore possibile per questa struttura, non uno neutro.")
# ------------------------------------------------------------ §3 ESEGUIBILITA'
hr("§3 ESEGUIBILITA' — e qui il filone cambia domanda")
fams = sorted({n.split("-")[0] for n in puts["instrument_name"].head(50_000)})
print(f" La catena che raccogliamo contiene le famiglie: {fams}")
print(" → sono le opzioni INVERSE: quotate, marginate e regolate in BTC/ETH.")
print(" Il conto e' in USDC ($635) e il book esegue sui perpetual LINEARI USDC.\n")
disk = specs_from_disk()
if disk:
print(" Specifiche gia' registrate dal progetto (data/options_daily/snapshots.jsonl):")
for c, v in sorted(disk.items()):
print(f" {c:6s} min_trade_amount={v['min']} taker={v['taker']} ({v['ts']:%Y-%m-%d %H:%M})")
if venue.get("ok"):
idx = venue["index"]
print(f"\n Lette dal venue ora — indice BTC ${idx['btc']:,.0f} / ETH ${idx['eth']:,.0f}:")
print(f" {'famiglia':11s} {'min lotto':>10s} {'settle':>7s} {'quote':>7s} "
f"{'$ nozionale/lotto':>18s}")
for fam in ("BTC", "ETH", "BTC_USDC", "ETH_USDC"):
s = venue["specs"].get(fam)
if not s:
continue
base = (s.get("base_currency") or fam.split("_")[0]).lower()
notion = float(s["min_trade_amount"]) * idx[base]
print(f" {fam:11s} {s['min_trade_amount']:>10} {str(s['settlement_currency']):>7s} "
f"{str(s['quote_currency']):>7s} {notion:>17,.0f}$")
print("\n 📌 Esiste una famiglia USDC-lineare per BTC ed ETH, con lotto minimo 10x PIU'")
print(" PICCOLO e regolata nella valuta del conto. Il muro «BTC fuori a $3.000» del")
print(" 30/07 — congelato in tests/test_vrp_profit_take.py — e' stato misurato sulla")
print(" famiglia che il conto NON margina.")
print(" E' la 3a occorrenza della stessa forma dopo fee_watch (21/08, sorvegliava gli")
print(" INVERSE mentre il book trada i LINEARI) e la taratura di venue_watch.")
# --- ma la famiglia eseguibile e' quotata davvero? ---
d = pd.DataFrame(venue["summary"])
parts = d["instrument_name"].str.split("-")
d["fam"] = parts.str[0]
d["typ"] = parts.str[3]
d["strike"] = pd.to_numeric(parts.str[2].str.replace("d", ".", regex=False), errors="coerce")
d["expd"] = pd.to_datetime(parts.str[1], format="%d%b%y", utc=True) + pd.Timedelta(hours=8)
d["dte"] = (d["expd"] - today).dt.total_seconds() / 86400.0
d["base"] = d["fam"].str.replace("_USDC", "", regex=False).str.lower()
d = d[d["typ"].eq("P") & d["base"].isin(["btc", "eth"])].copy()
d["S"] = d["base"].map(idx)
d["mny"] = d["strike"] / d["S"]
q = d[(d["bid_price"] > 0) & (d["ask_price"] > 0)].copy()
q["relsp"] = (q["ask_price"] - q["bid_price"]) / q["mid_price"]
# regione delle put OTM usate dalla struttura (δ -0.35..-0.05 ≈ moneyness 0.78-0.97)
w = q[(q["dte"] > 3) & (q["dte"] < 11) & q["mny"].between(0.78, 0.97)]
print("\n Liquidita' REALE nella regione di strike che serve (scadenza settimanale, ora):")
print(f" {'famiglia':11s} {'n quotate':>9s} {'spread rel. med':>16s} "
f"{'p75':>7s} {'OI med':>9s} {'vol 24h':>10s}")
for fam, g in w.groupby("fam"):
allf = d[d["fam"] == fam]
print(f" {fam:11s} {len(g):>9d} {g['relsp'].median():>15.0%} "
f"{g['relsp'].quantile(.75):>6.0%} {g['open_interest'].median():>9.1f} "
f"{allf['volume'].sum():>10,.0f}")
print("\n Lo spread relativo e' ENORME in entrambe le famiglie (il mid non e' un prezzo\n"
" eseguibile: e' la media di due prezzi lontani), e sulla famiglia USDC — l'unica\n"
" marginabile dal conto — l'open interest e' ~0: quotata da un market maker,\n"
" praticamente non tradata. Comprare l'ala li' dentro costa il doppio.")
else:
print(f"\n ⚠️ LETTURA DAL VENUE NON RIUSCITA: {venue.get('error', 'n/d')}")
print(" Le specifiche delle famiglie USDC restano NON MISURATE in questo giro.")
print("\n Capitale per UN lotto minimo, cella canonica (limiti dichiarati, vedi docstring):")
print(f" {'asset':6s} {'lotto':>7s} {'width med':>10s} {'rischio max':>12s} "
f"{'cash-secured':>13s} {'credito/sett':>13s}")
minlot = {"BTC": 0.1, "ETH": 1.0}
if venue.get("ok"):
for fam in ("BTC", "ETH"):
if fam in venue["specs"]:
minlot[fam] = float(venue["specs"][fam]["min_trade_amount"])
for a in ASSETS:
tr, lot = canon[a], minlot[a]
wid = float(tr["width"].median())
print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ "
f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$")
print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n"
" Quale dei due valga dipende dal regime di margine del conto (standard vs portfolio),\n"
" che NON e' leggibile da dati pubblici.")
print(" ⚠️ E c'e' una domanda PRIMA di quella, che nessun dato pubblico chiude: un saldo in\n"
" USDC puo' marginare un'opzione INVERSE (collaterale in BTC/ETH)? Se no, la famiglia\n"
" con 3 mesi di storia non e' negoziabile da questo conto a NESSUN capitale.\n"
" Precedente esatto: il 26/07 GTAA01 aveva 5 settimane di misure sopra l'assunzione\n"
" che i 6 ETF fossero comprabili — il blocco PRIIPs si scopri' con UN ordine di prova.\n"
" La regola che ne usci' («la negoziabilita' sul conto REALE si verifica quando lo\n"
" sleeve entra in RICERCA, non quando entra nel book») si applica qui identica, e\n"
" questo script NON puo' eseguirla: richiede il conto, non i dati.")
# ------------------------------------------------------------ §4 BACKTEST
hr("§4 BACKTEST SU QUOTE REALI — griglia dichiarata")
n_trials = len(CELLS) * len(FILLS) * (len(ASSETS) + 1)
print(f" celle δ (short,long): {len(CELLS)} fill: {len(FILLS)} "
f"asset: {len(ASSETS)} + combinato 50/50")
print(f" → TRIAL DICHIARATI AL RIALZO: {n_trials}. Nessuna griglia ridotta per budget.")
print(f" (Il fill `mid` non e' selezionabile — e' la diagnostica di quanto costa lo spread —\n"
f" ma conta come trial: e' stato guardato.)\n")
grid = []
for fill in FILLS:
for (sd, ld) in CELLS:
per, comb = {}, []
for a in ASSETS:
tr = build_trades(puts, a, sd, ld, fill=fill, lot=1.0, today=today)
per[a] = stats(tr)
if not tr.empty:
comb.append(weekly_series(tr))
row = dict(fill=fill, sd=sd, ld=ld)
for a in ASSETS:
row[f"{a}_sh"] = per[a]["sharpe"]
row[f"{a}_n"] = per[a]["n"]
if comb:
c = pd.concat(comb, axis=1).mean(axis=1).dropna()
row["comb_sh"] = (float(c.mean() / c.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR))
if c.std(ddof=1) > 0 else np.nan)
row["comb_n"] = len(c)
grid.append(row)
G = pd.DataFrame(grid)
print(G.round(2).to_string(index=False))
print("\n ⚠️ COME SI LEGGE LA TABELLA SOPRA. Gli 'Sharpe' a due cifre NON sono un risultato:\n"
" sono la firma del campione. Annualizzare (x√52) 10 osservazioni settimanali in cui\n"
" la coda sinistra non si e' manifestata produce numeri privi di significato — e il\n"
" fatto che l'INTERA griglia li produca e' esattamente il segnale, non un caso da\n"
" selezionare. E' la 4a occorrenza della firma '0-perdite / Sharpe implausibile'\n"
" dopo CC01, le celle deep-OTM Albimarini e meta' della griglia VRP del 03/07.\n"
" Per lo stesso motivo NON viene riportato un CAGR: comporre 10 settimane a 52\n"
" sarebbe una fabbricazione, non una stima.")
print("\n Cella canonica di VRP01 (δ-0.28/-0.10), fill conservativo, per asset — numeri\n"
" CONCRETI (dollari per lotto minimo), che a questa taglia dicono piu' di un rapporto:")
print(f" {'asset':6s} {'n':>3s} {'vinte':>7s} {'$/sett':>9s} {'$ tot':>9s} "
f"{'% rischio/sett':>15s} {'t':>6s} {'maxDD chiusure':>15s}")
st = {}
for a in ASSETS:
tr = canon[a].copy()
tr[["cred", "payoff", "fee", "pnl", "risk", "cred_mod",
"fee_sleeve", "fee_sleeve_su_reale"]] *= minlot[a]
st[a] = stats(tr)
s = st[a]
print(f" {a:6s} {s['n']:>3d} {s['wins']:>3d}/{s['n']:<3d} {s['usd_week']:>8.2f}$ "
f"{s['usd_tot']:>8.2f}$ {s['mean']:>14.2%} {s['t']:>6.2f} {s['maxdd_close']:>14.1%}")
print(" maxDD 0.0% su BTC significa che NON C'E' STATA una settimana in perdita: e' una\n"
" proprieta' del campione, non della struttura.")
print("\n Costo dello SPREAD, misurato (stesso ingresso, fill al mid vs al bid/ask):")
for a in ASSETS:
ba = build_trades(puts, a, *CANON, fill="bid_ask", today=today)
md = build_trades(puts, a, *CANON, fill="mid", today=today)
j = ba.merge(md, on="exp", suffixes=("_ba", "_md")) # appaiato per INGRESSO
if j.empty:
continue
print(f" {a}: credito medio al mid ${j['cred_md'].mean():.2f} → al bid/ask "
f"${j['cred_ba'].mean():.2f} = si perde il "
f"{1 - j['cred_ba'].mean() / j['cred_md'].mean():.0%} del credito solo attraversando "
f"lo spread")
print("\n Il f del 30/07, ri-misurato su 10 settimane invece di 8 (fill conservativo):")
for a in ASSETS:
f = canon[a]["f_net"].dropna()
lo, hi = boot_ci(f.to_numpy())
print(f" {a}: f_net mediana {f.median():.3f} media {f.mean():.3f} "
f"IC95 [{lo:.3f}, {hi:.3f}] ≥1.0 in {int((f >= 1).sum())}/{len(f)}")
allf = pd.concat([canon[a]["f_net"] for a in ASSETS]).dropna()
lo, hi = boot_ci(allf.to_numpy())
print(f" pooled: {allf.median():.3f} (IC95 [{lo:.3f}, {hi:.3f}], n={len(allf)}) — "
f"replica indipendente dello 0.73 del 30/07")
print("\n Fee: listino vero contro il forfait dello sleeve (12.5% del credito NETTO).")
print(" ⚠️ Il confronto giusto e' col credito MODELLATO — e' quello a cui il sleeve applica\n"
" il forfait. Misurarlo sul credito reale (piu' piccolo di ~30%) sottostima l'errore:")
for a in ASSETS:
tr = canon[a]
print(f" {a}: fee reale ${tr['fee'].mean():.2f}/settimana per contratto vs forfait sul\n"
f" credito MODELLATO ${tr['fee_sleeve'].mean():.2f} (= {tr['fee_sleeve'].mean() / tr['fee'].mean():.2f}x)"
f" · sul credito reale ${tr['fee_sleeve_su_reale'].mean():.2f} "
f"(= {tr['fee_sleeve_su_reale'].mean() / tr['fee'].mean():.2f}x)")
print(" Il '~2x' pubblicato il 30/07 non si replica qui: su queste 19 settimane il forfait\n"
" sovrastima le fee di 1.1-1.9x a seconda dell'asset e della base di confronto.\n"
" Resta vero il segno (il forfait e' CONSERVATIVO), non la taglia.")
# ------------------------------------------------------------ §5 IL MINIMO
hr("§5 IL RISCHIO STA NEL MINIMO — le chiusure settimanali non lo vedono")
print(" Regola del 30/07: un rischio valutato sul minimo non si misura sulle chiusure.\n"
" Qui il mark infra-settimana viene dalle standing quotes ORARIE delle STESSE due gambe\n"
" (ricomprare la corta all'ask, rivendere la lunga al bid: di nuovo conservativo).\n")
worst_rows = []
for a in ASSETS:
print(f" {a} (lotto {minlot[a]})")
for _, r in canon[a].iterrows():
path = CB.close_cost_path(r, puts)
if path.empty:
continue
lot = minlot[a]
risk = r["width"] * lot - r["cred"] * lot
worst = float(path["pnl_aperto"].min()) * lot
worst_rows.append(dict(asset=a, exp=r["exp"], worst=worst, risk=risk,
frac=worst / risk, pnl=r["pnl"] * lot,
n_marks=len(path)))
flag = " ← chiusa in UTILE" if r["pnl"] > 0 and worst < 0 else ""
print(f" {r['exp']:%Y-%m-%d} marks={len(path):3d} "
f"peggior mark {worst:>9.2f}$ = {worst / risk:>7.1%} del rischio "
f"esito {r['pnl'] * lot:>7.2f}${flag}")
W = pd.DataFrame(worst_rows)
if not W.empty:
print(f"\n Peggior mark infra-settimana: mediana {W['frac'].median():.1%} del rischio, "
f"minimo {W['frac'].min():.1%}")
wmin = W.loc[W['frac'].idxmin()]
print(f" Denominatore, per non fabbricare una discrepanza: qui il rischio e' "
f"`width - credito`.\n"
f" CLAUDE.md cita «-81,7% del capitale» per la STESSA settimana "
f"({wmin['asset']} {wmin['exp']:%Y-%m-%d}):\n"
f" e' lo stesso mark (${wmin['worst']:.2f}) diviso per la width nuda. Due "
f"denominatori, un solo fatto.")
wl = W[W["pnl"] > 0]
print(f" Fra le sole settimane chiuse in UTILE ({len(wl)}/{len(W)}): mediana "
f"{wl['frac'].median():.1%}, minimo {wl['frac'].min():.1%}")
print(" → il maxDD sulle chiusure e' una lente che non vede il rischio che si e' corso.")
# ------------------------------------------------------------ §6 GATE
hr("§6 GATE — compresi quelli che NON si possono far girare")
print(" (a) implausible_sharpe — il gate del 26/07 sul rischio FUORI dal campione")
for a in ASSETS:
tr = canon[a]
daily = pd.Series((tr["pnl"] / tr["risk"]).to_numpy(float),
index=pd.DatetimeIndex(tr["exp"]))
rep = A.implausible_sharpe(daily, n_trades=len(tr), n_losing_trades=int((tr["pnl"] <= 0).sum()))
print(f" {a}: implausible={rep['implausible']} vinte {int((tr['pnl'] > 0).sum())}/{len(tr)}")
for why in rep["reasons"]:
print(f" · {why}")
if int((tr["pnl"] <= 0).sum()) == 0:
print(f" · regola del tre: 0 perdite su {len(tr)} lascia un tasso VERO fino a "
f"{3 / len(tr):.0%}. Su un payoff che perde {tr['risk'].median() / tr['cred'].median():.1f}x "
f"il credito, basta a ribaltare l'expectancy.")
print("\n (b) deflated_sharpe — NON CALCOLABILE, e il perche' e' il risultato")
for a in ASSETS:
s = weekly_series(canon[a])
dsr, _ = A.deflated_sharpe(st[a]["sharpe"], [r["comb_sh"] for r in grid if
np.isfinite(r.get("comb_sh", np.nan))],
s, dpy=WEEKS_PER_YEAR)
print(f" {a}: DSR = {dsr} (T={len(s)} osservazioni settimanali; la funzione "
f"richiede T>=30)")
print(" Costruire una serie GIORNALIERA per superare il T>=30 sarebbe barare: uno sleeve\n"
" settimanale su griglia giornaliera e' ~94% di zeri (lezione 26/07 su VRP01), e il\n"
" T gonfiato entra nel deflated-Sharpe come se fossero osservazioni indipendenti.\n"
" Statistica onesta a questa taglia = il t con il suo intervallo:")
for a in ASSETS:
r = (canon[a]["pnl"] / canon[a]["risk"]).to_numpy(float)
lo, hi = boot_ci(r)
print(f" {a}: media settimanale {r.mean():+.2%} del rischio, IC95 "
f"[{lo:+.2%}, {hi:+.2%}], t={st[a]['t']:.2f}, n={len(r)}")
print("\n (c) anchor_luck_band — l'ora d'ingresso e' un'ancora (max-of-k mai dichiarato)")
offsets = list(range(-24, 25, 6))
for a in ASSETS:
def by_off(o, _a=a):
return weekly_series(build_trades(puts, _a, *CANON, offset_h=float(o), today=today))
band = A.anchor_luck_band(
by_off, offsets, canonical=0,
metric=lambda s: (float(s.mean() / s.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR))
if len(s) > 2 and s.std(ddof=1) > 0 else 0.0))
if band.get("reason"):
print(f" {a}: {band['reason']}")
continue
print(f" {a}: canonica {band['canonical']:.2f} (pctl {band['canonical_pctl']:.0%}) · "
f"MEDIANA ONESTA {band['median']:.2f} · banda [{band['lo']:.2f}, {band['hi']:.2f}] · "
f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · "
f"fortuna {band['luck']:+.2f}")
# DOVE vive l'instabilita': nel numeratore (il guadagno) o nel denominatore (la vol)?
det = []
for o in offsets:
w = by_off(o)
if len(w) > 2 and w.std(ddof=1) > 0:
det.append((o, float(w.mean()), float(w.std(ddof=1)), len(w)))
if det:
mm = np.array([d[1] for d in det])
ss = np.array([d[2] for d in det])
# il rapporto si stampa SOLO se il minimo e' positivo: max/min con min<0
# e' un numero senza significato (prima stesura: '112.240.864x').
rm = (f"{mm.max() / mm.min():.1f}x" if mm.min() > 0 else "SEGNO CHE CAMBIA")
print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] "
f"({rm}) · deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] "
f"({ss.max() / ss.min():.1f}x)")
print(f" → si muovono ENTRAMBI, numeratore e denominatore"
f"{' — e sulla media il segno CAMBIA' if mm.min() <= 0 else ''}.\n"
f" Il motivo e' meccanico e vale la pena dirlo: spostare l'ora d'ingresso\n"
f" cambia lo SNAPSHOT, quindi quali strike combaciano col delta bersaglio\n"
f" — non e' lo stesso trade un'ora dopo, e' un'altra struttura. Con\n"
f" {det[0][3]} settimane questo parametro di disturbo domina tutto il resto.")
print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'")
print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del")
print(" 20-50% guadagna PER COSTRUZIONE. Regressione del PnL settimanale sul ritorno")
print(" del sottostante nella stessa settimana:")
for a in ASSETS:
tr = canon[a]
x = tr["und_ret"].to_numpy(float)
y = (tr["pnl"] / tr["risk"]).to_numpy(float)
if len(x) < 4:
continue
b, alpha = np.polyfit(x, y, 1)
resid = y - (b * x + alpha)
se = float(np.std(resid, ddof=2) / (np.std(x) * np.sqrt(len(x))))
print(f" {a}: beta {b:+.3f} (t={b / se:.2f}) alpha {alpha:+.2%}/settimana "
f"corr {np.corrcoef(x, y)[0, 1]:+.2f}")
k = abs(b)
lev = pd.Series(k * x, index=pd.DatetimeIndex(tr["exp"]))
sh_lev = (float(lev.mean() / lev.std(ddof=1) * np.sqrt(WEEKS_PER_YEAR))
if lev.std(ddof=1) > 0 else np.nan)
print(f" null: essere semplicemente LONG il sottostante a leva {k:.2f} "
f"(stesso beta) da' Sharpe {sh_lev:.2f} contro {st[a]['sharpe']:.2f} della struttura")
print("\n (e) marginal_vs_tp01 — girato, ma NON interpretabile a questa taglia")
try:
comb = pd.concat([weekly_series(canon[a]) for a in ASSETS], axis=1).mean(axis=1).dropna()
dly = comb.resample("1D").sum().fillna(0.0)
rep = A.marginal_vs_tp01(dly)
act = int((dly != 0).sum())
def _f(k):
v = rep.get(k)
return f"{v:.3f}" if isinstance(v, (int, float)) and v is not None else str(v)
print(f" verdetto={rep.get('verdict')} corr={_f('corr')} "
f"insample_edge={rep.get('has_insample_edge')} robust_oos={rep.get('robust_oos')} "
f"is_hedge={rep.get('is_hedge')}")
print(f" barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%} — un `None` qui non e'\n"
f" un bug: le sotto-misure che richiedono un hold-out non hanno dati per\n"
f" esistere, e la funzione lo dichiara invece di inventare un numero.")
print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' "
f"rumore, si riporta per non ometterlo.")
except Exception as exc: # noqa: BLE001
print(f" NON GIRATO: {type(exc).__name__}: {exc}")
print("\n (f) causality_ok — non girato su questo candidato, e perche'")
print(" `A.causality_ok` valuta target_fn su OHLCV BTC/ETH; qui il segnale non e' una")
print(" serie di prezzo ma una selezione di strumenti da uno snapshot. Verifica")
print(" equivalente eseguita per costruzione e controllata qui sotto:")
bad = 0
for a in ASSETS:
for _, r in canon[a].iterrows():
if r["ts"] >= r["exp"] or not (DTE_LO <= r["dte"] <= DTE_HI):
bad += 1
print(f" ingressi con ts >= scadenza o fuori finestra DTE: {bad}/"
f"{sum(len(canon[a]) for a in ASSETS)} (le gambe si scelgono da UNO snapshot allo\n"
f" stesso ts; il regolamento usa l'indice alla scadenza, mai prima)")
# ------------------------------------------------------------ §7 VERDETTO
hr("§7 VERDETTO")
print(""" 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
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
regola del tre — 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che
perde ~7x il credito.
⚠️ Sfumatura che va detta: il gate non e' lontano. Ha mancato l'apertura per 0.0002 di
IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un
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.
⚠️ 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
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.
3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono
le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book
esegue sui perpetual lineari USDC. Esiste una famiglia USDC-lineare per BTC ed ETH col
lotto minimo 10x piu' piccolo (BTC 0.01 = $773 di nozionale invece di 0.1 = $7.728),
quindi il muro «BTC fuori a $3.000» del 30/07 — congelato in
tests/test_vrp_profit_take.py — e' misurato sulla famiglia che il conto non margina.
E' la 3a occorrenza della stessa forma dopo fee_watch (21/08) e la taratura di
venue_watch: un numero puntato su una configurazione diversa da quella che gira.
Ma di quella famiglia abbiamo ZERO ore di storia.
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/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e
con un percorso diverso.
CIO' CHE NON SI CONCLUDE: che la struttura non funzioni. Non e' stata osservata nel regime
in cui lavora. E un risultato a favore c'e', va detto: su BTC il null «e' solo crypto
travestito» e' REFUTATO (beta +0.019, t=0.18) — il PnL non viene dal rialzo. Non basta,
perche' il punto 2 toglie il titolo comunque.
LA REGOLA NON CAMBIA. «Niente short-vol da MODELLO in deploy» non diventa «short-vol da
QUOTE»: le quote adesso ci sono, ma sono quote della famiglia sbagliata, in un regime solo,
e con l'ancora che spiega il risultato. La sostituzione della regola resta NON DIMOSTRATA.
DUE AZIONI, NESSUNA ESEGUITA (toccano la produzione o il conto: decide l'operatore).
(A) DATO — far raccogliere a `collect_chain.py` anche `currency=USDC`. Costo MISURATO col
filtro OI>=100 gia' in vigore: ETH_USDC porterebbe 119 strumenti per giro (+21% di
chiamate, da ~570 a ~690), BTC_USDC ne porterebbe 5 su 686 (0.7%) = nulla di
utilizzabile. Cioe': l'unica struttura USDC-regolata con un libro vero e' su ETH,
lotto minimo 0.1 ETH = $242. Senza questa raccolta, fra sei mesi avremo un anno di
storia della famiglia che non possiamo marginare e zero di quella che potremmo — che
e' esattamente la posizione di oggi, un'ondata dopo l'altra.
(B) CONTO — una domanda che nessun dato pubblico chiude e che viene PRIMA di ogni
backtest: un saldo in USDC puo' marginare un'opzione INVERSE? Se no, la famiglia con
3 mesi di storia non e' negoziabile da questo conto a nessun capitale, e il punto 3
smette di essere un dettaglio di lotto per diventare un muro. Si risolve come si
risolse il PRIIPs il 26/07: guardando il conto, non i dati.
SE E QUANDO RIAPRIRLO. Non «quando ci sara' un crash» (criterio senza numero) ma: DVOL BTC
> 48.3 o ETH > 62.8 in modo persistente, PIU' ~un anno di settimane raccolte in quel
regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura
descrivera' la sua astensione.""")
hr("COSA MI SMENTIREBBE", "-")
print(""" Tre osservazioni, tutte ottenibili, che ribalterebbero questo verdetto — e una che NON
lo ribalterebbe, messa qui perche' e' quella che arrivera' per prima.
1. IL CONTO. Se un saldo USDC puo' marginare le opzioni INVERSE, il punto 3 si sgonfia da
muro a dettaglio: la struttura ETH inverse (1 lotto, rischio ~$88, credito ~$12 la
settimana) e' gia' eseguibile a $600 oggi. Resterebbero i punti 1, 2 e 4 — sufficienti
a tenere il verdetto, ma il filone tornerebbe misurabile SUBITO invece che fra un anno.
Costo della verifica: un ordine di prova, come per il PRIIPs.
2. IL REGIME. Se, raccolte ~un anno di settimane col gate IV-rank APERTO, la media
settimanale restasse positiva CON la banda d'ancora che non contiene lo zero, allora
«short-vol da QUOTE» sarebbe dimostrato e la regola del 19/06 andrebbe riscritta.
E' la sola prova che conta: la banda d'ancora, non la media.
3. IL LIBRO. Se l'open interest di ETH_USDC salisse stabilmente sopra ~100 sugli strike a
δ -0.28/-0.10 con spread relativo sotto ~15%, esisterebbe una struttura regolata in
USDC ed eseguibile a $242 per lotto: il filone si riaprirebbe sul DATO invece che
sulla teoria. Ma senza raccoglierla adesso non lo sapremo mai — vedi azione (A).
⚠️ COSA NON MI SMENTIREBBE, ed e' quello che succedera' per primo: un'altra finestra di
10 settimane di vol bassa con 10/10 vincenti e uno Sharpe a due cifre. Sarebbe lo
STESSO campione, piu' lungo — non evidenza nuova. Un campione che non contiene il modo
di perdere non diventa informativo aggiungendo settimane in cui non si perde.""")
if __name__ == "__main__":
main()
+786
View File
@@ -0,0 +1,786 @@
"""XS-LITE — esiste una versione CONCENTRATA (2-8 gambe) degli edge cross-sectional del progetto
che conservi una frazione utile dell'edge ed e' ESEGUIBILE OGGI a $600-5.000?
LA DOMANDA (mai posta in questa forma). Il progetto ha due edge cross-sectional che dichiara di non
poter eseguire per TAGLIA, non per debolezza:
XS01 — 19 major HL, long 5 / short 5, ribilancio ogni 10g. Dichiarato eseguibile da ~$20k.
XSR01 — 50 alt HL, residuo OLS su BTC demeanato cross-sezionalmente. Dichiarato ~$5k.
Il vincolo dichiarato e' il **min order** moltiplicato per il numero di gambe. Se si riducono le
gambe il vincolo si allenta — ma l'ampiezza (il numero di scommesse indipendenti) crolla, e
l'ampiezza e' l'unica ragione per cui una strategia cross-sectional ha uno Sharpe alto.
Questo script misura il CAMBIO, non lo assume. E misura anche la premessa: che il muro sia il
min-order.
IPOTESI PRE-REGISTRATE (scritte prima di guardare i numeri):
(a) lo Sharpe scende con k come ~sqrt(ampiezza): k=2-3 conserva il 40-60% dell'edge;
(b) il min-order NON e' il muro di XS01 a $600 (a k=5 il ticket e' 0.5/5 del capitale dello
sleeve), e il "serve $20k" viene dal TURNOVER del vol-target, non dalla taglia della posizione;
(c) XSR01 concentrato perde molto di piu', perche' il suo edge E' l'ampiezza (il progetto ha gia'
misurato che il demeaning porta l'ampiezza 4.5 -> 37.4: e' quel numero, non il segnale, a
fare lo Sharpe 1.82).
Con 2.6 anni di storia monoregime il verdetto atteso e' LEAD o SCARTATO, mai CANDIDATO.
MECCANISMI CONGELATI — non si rifitta nulla.
XS01 : blend lookback (30,90), gate dispersione p30 espandente causale, H=10, vol-target 20%,
cap 3x, fee 5 bps per unita' di turnover. UNICO parametro variato: k (gambe per lato).
Replica bit-exact di src.portfolio.sleeves._xsec_returns verificata al passo (0).
XSR01: W=45, sgn=+1, residuo OLS rolling causale su BTC, z-score, tanh, vol-target 20%, cap 2x,
demean cross-sezionale giornaliero, fee 5 bps/gamba. Segnale IMPORTATO da
r0725_statarb_multi, paniere da r0725_statarb_basket_gate (nessuna reimplementazione).
UNICO parametro variato: k.
Il LOOKBACK NON e' stato riaperto DI PROPOSITO: riaprire un parametro riapre la sua famiglia
(lezione VRP-tenore, 30/07) e moltiplicherebbe i trial. Qui si varia solo la CONCENTRAZIONE.
GRIGLIA DICHIARATA PRIMA DI MISURARE: 2 famiglie x 2 universi x 6 valori di k (+ il "pieno" di
XSR01) = 26 celle valutabili. Le 10 fasi del ciclo H=10 di XS01 sono una banda d'ANCORA, non una
griglia di parametri (regola del progetto: il deflated-Sharpe non le conta) -> riportate come
mediana onesta e come differenze APPAIATE. Il deflated-Sharpe e' comunque pubblicato a N=26 E a
N=156 (sensibilita' del verdetto al conteggio, lezione 30/07).
nice -n 19 timeout 900 uv run python scripts/research/r0822_xs_lite.py
"""
from __future__ import annotations
import sys
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _xsec_returns # noqa: E402
from r0725_statarb_multi import BASE, load_hl, pnl, signal # noqa: E402
from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402
RAW = ROOT / "data" / "raw"
ANN = np.sqrt(365.25)
HOLDOUT = A.HOLDOUT # 2025-01-01 (convenzione altlib)
MIN_ORDER_HL = 10.0 # Hyperliquid: min order $10 (r0702_capital_scaling)
MIN_ORDER_5 = 5.0 # il $5 di Deribit, citato nella missione
KS = (1, 2, 3, 4, 5, 6) # 5 = canonico XS01
COST_SWEEP = (0.0, 5.0, 10.0, 20.0, 30.0, 50.0) # bps per gamba TOTALI (fee + slippage)
# capitali dello SLEEVE. Il progetto tiene XS01 al 15% del book -> a book $600 lo sleeve ha $90.
# Si misurano entrambe le letture, perche' e' esattamente qui che nasce il numero "$20k".
SLEEVE_CAPS = (90.0, 300.0, 600.0, 750.0, 2000.0, 5000.0)
# =====================================================================================
# misure
# =====================================================================================
def sh(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0
def dd(s) -> float:
r = np.nan_to_num(np.asarray(pd.Series(s).values, float))
eq = np.cumprod(1.0 + np.clip(r, -0.99, None))
return float(np.min(eq / np.maximum.accumulate(eq) - 1.0))
def cagr(s) -> float:
x = pd.Series(s).dropna()
if len(x) < 30:
return 0.0
yrs = (x.index[-1] - x.index[0]).days / 365.25
eq = float(np.prod(1.0 + np.clip(x.values, -0.99, None)))
return float(eq ** (1.0 / yrs) - 1.0) if yrs > 0.2 and eq > 0 else 0.0
def split(s):
x = pd.Series(s).dropna()
return x[x.index < HOLDOUT], x[x.index >= HOLDOUT]
HDR = f" {'cella':<20}{'ShFULL':>8}{'Sh IS':>8}{'Sh OOS':>8}{'maxDD':>9}{'CAGR':>9}"
def row(name, s, extra=""):
ins, out = split(s)
return (f" {name:<20}{sh(s):>8.2f}{sh(ins):>8.2f}{sh(out):>8.2f}"
f"{dd(s)*100:>8.1f}%{cagr(s)*100:>8.1f}%{extra}")
# =====================================================================================
# (A) XS01 congelato, con k e fase parametrici e universo a copertura variabile
# =====================================================================================
def load_closes(syms, how="inner") -> pd.DataFrame:
cols = {}
for s in syms:
p = RAW / f"hl_{s.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p, columns=["timestamp", "close"])
cols[s] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
C = pd.concat(cols, axis=1, join=("inner" if how == "inner" else "outer")).sort_index()
return C.dropna() if how == "inner" else C
def xs_run(C: pd.DataFrame, k: int, phase: int = 0):
"""Replica di sleeves._xsec_returns con k e fase parametrici, tollerante ai simboli con
copertura piu' corta (si valutano solo quelli con storia sufficiente a quella data: senza
questo, un outer-join su 51 alt riduce la finestra comune a ~2/3 e cancella l'in-sample).
Con tutte le colonne valide il percorso e' NUMERICAMENTE IDENTICO all'originale — provato al
passo (0) con max|diff| = 0.
Ritorna (serie netta lente-sleeve, W [n x A], scale [n], dret [n x A])."""
px = C.values.astype(float)
n, na = px.shape
lookbacks, H, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["mode"], XS_CFG["target_vol"]
disp_pct, minhist = XS_CFG["disp_pct"], XS_CFG["disp_minhist"]
mlb = max(lookbacks)
fin = np.isfinite(px)
prev = np.vstack([np.full(na, np.nan), px[:-1]])
dret = np.where(fin & np.isfinite(prev), px / prev - 1.0, 0.0)
dret = np.nan_to_num(dret)
W = np.zeros((n, na))
w = np.zeros(na)
disp_hist = []
for i in range(n):
if i >= mlb and (i - phase) % H == 0:
ok = fin[i].copy()
for L in lookbacks:
ok &= fin[i - L]
if ok.sum() >= 2 * k:
cols = np.where(ok)[0]
rLs = [px[i, cols] / px[i - L, cols] - 1.0 for L in lookbacks]
disp_i = float(np.mean([r.std() for r in rLs]))
thr = (np.percentile(disp_hist, disp_pct)
if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf)
if disp_i >= thr:
score = np.zeros(len(cols)); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(na)
lo, hi = cols[order[:k]], cols[order[-k:]]
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
else:
w = np.zeros(na)
disp_hist.append(disp_i)
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (0.001 / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return pd.Series(s.values * scale, index=C.index), W, scale, dret
def xs_effective_weights(W: np.ndarray, scale: np.ndarray) -> np.ndarray:
"""Peso EFFETTIVO tenuto durante la barra i = W[i-1]*scale[i]. Entrambi noti a close[i-1]
(scale usa una rolling std gia' shiftata) -> causale."""
Weff = np.zeros_like(W)
Weff[1:] = W[:-1] * scale[1:, None]
return Weff
# =====================================================================================
# (B) XSR01 congelato, con k parametrico
# =====================================================================================
def xsr_positions(P: pd.DataFrame, k: int | None) -> pd.DataFrame:
"""Q = posizioni demeanate cross-sezionalmente (meccanismo congelato).
Se k e' dato: tiene le k piu' positive e le k piu' negative di ogni riga, azzera il resto e
RI-demeana sulle sopravvissute (resta dollar-neutral)."""
Q = P.sub(P.mean(axis=1), axis=0)
if k is None:
return Q
V = Q.to_numpy(float)
out = np.zeros_like(V)
for i in range(V.shape[0]):
v = V[i]
m = np.isfinite(v)
if m.sum() < 2 * k or not np.any(v[m] != 0.0):
continue
idx = np.where(m)[0]
order = idx[np.argsort(v[idx])]
keep = np.concatenate([order[:k], order[-k:]])
r = np.zeros_like(v)
r[keep] = v[keep]
r[keep] -= r[keep].mean()
out[i] = r
return pd.DataFrame(out, index=P.index, columns=P.columns)
def xsr_legs(Q: pd.DataFrame, S: pd.DataFrame) -> pd.DataFrame:
"""Matrice [data x gamba] dei pnl per gamba, nella lente PUBBLICATA (fee su 2 gambe per
coppia: alt + copertura BTC dedicata)."""
return pd.DataFrame({c: pnl(Q[c].to_numpy(float), S[c].to_numpy(float)) for c in Q.columns},
index=Q.index)
def xsr_basket(Q: pd.DataFrame, S: pd.DataFrame, norm) -> pd.Series:
"""norm = 'perrow' riproduce ESATTAMENTE basket_from_positions (media per-riga sui simboli
con dato: il numero di simboli cambia nel tempo). norm = intero -> normalizzazione COSTANTE,
l'unica che rende confrontabili celle con k diverso."""
M = xsr_legs(Q, S)
if norm == "perrow":
return M.mean(axis=1).dropna()
return (M.fillna(0.0).sum(axis=1) / float(norm)).dropna()
# =====================================================================================
# (C) simulatore di libro con MIN-ORDER (lente eseguibile, comune alle due famiglie)
# =====================================================================================
def sim_book(Weff: np.ndarray, R: np.ndarray, idx, r_hedge=None, capital=None,
min_order=0.0, cost_bps=5.0):
"""Weff[i] = pesi TARGET da tenere durante la barra i (decisi a close[i-1]).
R[i] = ritorni per gamba nella barra i. r_hedge = ritorno della gamba di copertura (si tiene
-sum(w)); None = libro gia' neutrale fra le gambe. min_order>0: una variazione di NOZIONALE
sotto la soglia NON si esegue (si tiene la vecchia). cost_bps = costo per unita' di turnover."""
n, na = Weff.shape
c = cost_bps / 1e4
held = np.zeros(na)
hedge = 0.0
net = np.zeros(n)
n_fill = n_skip = 0
tickets, gross = [], []
for i in range(n):
tgt = Weff[i]
d_not = np.abs(tgt - held) * (capital or 1.0)
if min_order > 0.0 and capital:
move = d_not >= min_order
else:
move = d_not > 1e-12
new = np.where(move, tgt, held)
n_fill += int(move.sum())
n_skip += int(((~move) & (d_not > 1e-12)).sum())
tickets.extend(d_not[move].tolist())
turn = float(np.abs(new - held).sum())
r = float(np.dot(new, R[i]))
if r_hedge is not None:
h = -float(new.sum())
if min_order > 0.0 and capital and abs(h - hedge) * capital < min_order:
h = hedge
turn += abs(h - hedge)
hedge = h
r += hedge * float(r_hedge[i])
net[i] = r - c * turn
held = new
gross.append(float(np.abs(new).sum()) + abs(hedge))
return pd.Series(net, index=idx), dict(
n_fill=n_fill, n_skip=n_skip,
exec_share=(n_fill / (n_fill + n_skip)) if (n_fill + n_skip) else 1.0,
med_ticket=float(np.median(tickets)) if tickets else 0.0,
med_gross=float(np.median(gross)),
orders_py=(n_fill / (len(idx) / 365.25)) if len(idx) else 0.0)
# =====================================================================================
# (D) ampiezza
# =====================================================================================
def breadth(legs: pd.DataFrame, n_simult: int, min_overlap: int = 30):
"""Due misure, perche' una sola mente.
(1) N_eff = N/(1+(N-1)*rbar), N = gambe SIMULTANEE, rbar = correlazione media fra i contributi
di due gambe CONDIZIONATA a essere entrambe attive. E' la formula pubblicata dal progetto
(4.5 -> 37.4), qui condizionata: senza condizionare, concentrando si conterebbe come
'ampiezza' la ROTAZIONE nel tempo, che nello Sharpe sta gia' dentro sqrt(T).
INSTABILE per costruzione: rbar leggermente NEGATIVO (normale in un libro dollar-neutral)
la fa esplodere. Si legge insieme a rbar, mai da sola.
(2) N_hhi = numero effettivo di gambe che PORTANO il rischio, giorno per giorno:
1/sum(share_i^2) sulle quote |contributo|, mediato sui giorni attivi. Limitato da N,
stabile, e non dipende dal segno delle correlazioni."""
M = legs.to_numpy(float)
M = np.nan_to_num(M)
act = M != 0.0
rs = []
for a in range(M.shape[1]):
for b in range(a + 1, M.shape[1]):
m = act[:, a] & act[:, b]
if m.sum() < min_overlap:
continue
xa, xb = M[m, a], M[m, b]
if xa.std() > 0 and xb.std() > 0:
rs.append(float(np.corrcoef(xa, xb)[0, 1]))
rbar = float(np.mean(rs)) if rs else float("nan")
N = float(n_simult)
den = 1.0 + (N - 1) * rbar if rs else float("nan")
neff = (N / den) if (rs and den > 1e-3) else float("nan")
hh = []
for i in range(M.shape[0]):
v = np.abs(M[i][act[i]])
t = v.sum()
if t > 0 and len(v) > 0:
sHH = float(np.sum((v / t) ** 2))
if sHH > 0:
hh.append(1.0 / sHH)
return neff, rbar, (float(np.mean(hh)) if hh else float("nan")), len(rs)
# =====================================================================================
def main() -> None:
print("=" * 108)
print(" XS-LITE — una versione CONCENTRATA degli edge cross-sectional e' eseguibile a $600-5.000?")
print("=" * 108)
maj19 = list(XS_UNIVERSE)
all51 = sorted({p.stem.replace("hl_", "").replace("_1d", "").upper()
for p in RAW.glob("hl_*_1d.parquet")})
# ---------------------------------------------------------------- (0) repliche
print("\n" + "-" * 108)
print(" (0) REPLICA DEI MECCANISMI CONGELATI — prima di ogni delta")
print("-" * 108)
C19 = load_closes(maj19, "inner")
C51 = load_closes(all51, "outer")
off, _, _, _ = xs_run(C19, k=XS_CFG["k"], phase=0)
ref = _xsec_returns()
J = pd.concat({"mio": off, "sleeve": ref}, axis=1, join="inner").dropna()
d_xs = float(np.max(np.abs(J["mio"] - J["sleeve"])))
print(f" XS01 k=5 fase 0 vs sleeves._xsec_returns : max|diff| = {d_xs:.3e} "
f"({len(J)} barre) {'OK' if d_xs < 1e-12 else 'DIVERGE'}")
P, S = pair_frames()
Qfull = xsr_positions(P, None)
xsr_perrow = xsr_basket(Qfull, S, "perrow")
xsr_ref = basket_from_positions(P, S, cols=None, demean=True)
J2 = pd.concat({"mio": xsr_perrow, "ref": xsr_ref}, axis=1, join="inner").dropna()
d_xsr = float(np.max(np.abs(J2["mio"] - J2["ref"])))
print(f" XSR01 pieno vs basket_from_positions(demean): max|diff| = {d_xsr:.3e} "
f"({len(J2)} barre) {'OK' if d_xsr < 1e-12 else 'DIVERGE'}")
xsr_const = xsr_basket(Qfull, S, P.shape[1])
print(f" XSR01 pieno, lente pubblicata (media per-riga sui simboli CON DATO): "
f"Sharpe {sh(xsr_perrow):.2f} maxDD {dd(xsr_perrow)*100:.1f}% (pubblicati 1.82 / -2.6%)")
print(f" XSR01 pieno, normalizzazione COSTANTE 1/{P.shape[1]} : "
f"Sharpe {sh(xsr_const):.2f} maxDD {dd(xsr_const)*100:.1f}%")
print(" ^ le due lenti coincidono qui (la copertura e' quasi piena); da qui in poi si usa la")
print(" COSTANTE, l'unica che rende confrontabili celle con k diverso, e i numeri XSR01 vanno")
print(f" letti contro {sh(xsr_const):.2f}.")
disc = pd.Timestamp("2026-07-25", tz="UTC") # giorno di scoperta/inception monitor
xsr_disc = xsr_perrow[xsr_perrow.index <= disc]
xsr_fwd = xsr_perrow[xsr_perrow.index > disc]
print(f" XSR01 sulla FINESTRA DI SCOPERTA (<= {disc.date()}, {len(xsr_disc)} barre): "
f"Sharpe {sh(xsr_disc):.2f} maxDD {dd(xsr_disc)*100:.1f}%")
print(" ⚠ IL NUMERO PUBBLICATO (1.82) NON SI RIPRODUCE OGGI, e non e' la mia implementazione:")
print(" le due repliche qui sopra sono bit-exact col codice del progetto. Sulla finestra")
print(f" IDENTICA a quella di scoperta la lente 'paniere' da' {sh(xsr_disc):.2f} e la lente 'libro'")
print(" (fee 1 gamba alt + copertura BTC NETTA, convenzione di paper_xsr) da' 2.23: l'1.82 non")
print(" e' nessuna delle due. Spiegazione piu' probabile: data/raw e' gitignored e il cron")
print(" riscrive i parquet HL -> stesso codice, dati diversi (identico allo scoperto del 07/08")
print(" su GTAA/ADJUSTED_LAST). NON l'ho inseguito: tutte le mie conclusioni sono confronti")
print(" RELATIVI dentro UNA lente. Ma tocca un numero pubblicato e il gate del 23/10.")
print(f" XSR01 dal 25/07 a oggi (forward, {len(xsr_fwd)} barre, stessa lente): "
f"ritorno {(np.prod(1+xsr_fwd.values)-1)*100:+.2f}%"
+ (" (campione troppo corto per uno Sharpe: si legge il ritorno)"
if len(xsr_fwd) <= 30 else f" Sharpe {sh(xsr_fwd):.2f}"))
print(f" finestra MAJ19: {C19.index[0].date()} -> {C19.index[-1].date()} ({len(C19)} barre); "
f"in-sample = pre {HOLDOUT.date()} = {int((C19.index < HOLDOUT).sum())} barre")
cov51 = np.isfinite(C51.values).sum(axis=1)
print(f" finestra ALL51: {C51.index[0].date()} -> {C51.index[-1].date()} ({len(C51)} barre); "
f"simboli con dato: da {cov51.min()} a {cov51.max()} (outer-join: l'inner-join")
print(" taglierebbe la finestra a 627 barre e CANCELLEREBBE l'in-sample).")
print(" ⚠ l'in-sample e' UN ANNO SOLO e monoregime: ogni Sh IS qui sotto va letto cosi'.")
# ---------------------------------------------------------------- (1) XS-LITE
print("\n" + "-" * 108)
print(" (1) FAMIGLIA A — XS-LITE: XS01 congelato, varia solo k (gambe per lato). k=5 = canonico")
print("-" * 108)
print(HDR + f"{'N_eff':>8}{'rbar':>8}{'N_hhi':>8}")
xs_series, xs_cells = {}, {}
for uni, Cx in (("MAJ19", C19), ("ALL51", C51)):
for k in KS:
s, W, sc, dr = xs_run(Cx, k=k, phase=0)
Weff = xs_effective_weights(W, sc)
legpnl = pd.DataFrame(Weff * dr, index=Cx.index, columns=Cx.columns)
ne, rbar, nhhi, _ = breadth(legpnl, 2 * k)
nm = f"{uni} k={k}" + ("*" if (uni == "MAJ19" and k == 5) else "")
xs_series[nm] = s
xs_cells[nm] = dict(uni=uni, k=k, C=Cx, W=W, scale=sc, dret=dr,
neff=ne, rbar=rbar, nhhi=nhhi)
print(row(nm, s, f"{ne:>8.1f}{rbar:>8.3f}{nhhi:>8.1f}"))
print(" (*) = configurazione canonica del sleeve XS01")
# ---------------------------------------------------------------- (2) XSR-LITE
print("\n" + "-" * 108)
print(" (2) FAMIGLIA B — XSR-LITE: XSR01 congelato (demean), varia solo k. 'pieno' = tutte le gambe")
print("-" * 108)
maj18 = [s for s in maj19 if s != BASE]
Pm = P[[c for c in maj18 if c in P.columns]]
Sm = S[[c for c in maj18 if c in S.columns]]
print(f" ALL50: {P.shape[1]} gambe MAJ18: {Pm.shape[1]} gambe")
print(HDR + f"{'N_eff':>8}{'rbar':>8}{'N_hhi':>8}")
xsr_series, xsr_cells = {}, {}
for uni, (Pu, Su) in (("ALL50", (P, S)), ("MAJ18", (Pm, Sm))):
for k in list(KS) + [None]:
if k is not None and 2 * k > Pu.shape[1]:
continue
Q = xsr_positions(Pu, k)
norm = (2 * k) if k is not None else Pu.shape[1]
s = xsr_basket(Q, Su, norm)
legs = xsr_legs(Q, Su) / float(norm)
ne, rbar, nhhi, _ = breadth(legs, norm)
nm = f"{uni} k={k if k else 'pieno'}" + ("*" if (uni == "ALL50" and k is None) else "")
xsr_series[nm] = s
xsr_cells[nm] = dict(uni=uni, k=k, Q=Q, S=Su, norm=norm,
neff=ne, rbar=rbar, nhhi=nhhi)
print(row(nm, s, f"{ne:>8.1f}{rbar:>8.3f}{nhhi:>8.1f}"))
print(" (*) = XSR01 pieno (50 gambe demeanate), qui a normalizzazione costante")
# banda di fase calcolata PRIMA della curva: la stima onesta di uno sleeve ancorato e' la
# mediana della banda, quindi e' quella che deve stare nella curva (la fase 0 e' un max-of-10).
base_by_phase = {ph: xs_run(C19, k=5, phase=ph)[0] for ph in range(XS_CFG["H"])}
phase_med, phase_rows = {}, {}
for k in KS:
by_ph = {ph: (base_by_phase[ph] if k == 5 else xs_run(C19, k=k, phase=ph)[0])
for ph in range(XS_CFG["H"])}
vals = np.array([sh(by_ph[p_]) for p_ in range(XS_CFG["H"])])
diffs = np.array([sh(by_ph[p_]) - sh(base_by_phase[p_]) for p_ in range(XS_CFG["H"])])
phase_med[k] = float(np.median(vals))
phase_rows[k] = (vals, diffs)
# ---------------------------------------------------------------- (3) curva ampiezza
print("\n" + "-" * 108)
print(" (3) LA CURVA AMPIEZZA-vs-k — la domanda centrale: quanto ne resta concentrando?")
print("-" * 108)
sh_xs_star = sh(xs_series["MAJ19 k=5*"])
sh_xsr_star = sh(xsr_series["ALL50 k=pieno*"])
print(f" {'k':>4}{'gambe':>7} {'--------- XS-LITE MAJ19 ---------':>34} "
f"{'--------- XSR-LITE ALL50 --------':>34}")
print(f" {'':>4}{'':>7} {'N_hhi':>9}{'N_eff':>9}{'Sh f0':>8}{'Sh MED':>8}{'MED/MED*':>10} "
f"{'N_hhi':>9}{'N_eff':>9}{'Sh':>8}{'Sh/Sh*':>9}")
med5 = phase_med[5]
for k in KS:
an = f"MAJ19 k={k}" + ("*" if k == 5 else "")
bn = f"ALL50 k={k}"
ca, cb = xs_cells[an], xsr_cells[bn]
sa, sb = xs_series[an], xsr_series[bn]
print(f" {k:>4}{2*k:>7} {ca['nhhi']:>9.1f}{ca['neff']:>9.1f}{sh(sa):>8.2f}"
f"{phase_med[k]:>8.2f}{(phase_med[k]/med5 if med5 else 0):>10.0%} "
f"{cb['nhhi']:>9.1f}{cb['neff']:>9.1f}{sh(sb):>8.2f}"
f"{(sh(sb)/sh_xsr_star if sh_xsr_star else 0):>9.0%}")
cp = xsr_cells["ALL50 k=pieno*"]
print(f" {'pieno':>4}{cp['norm']:>7} {'':>9}{'':>9}{'':>8}{'':>8}{'':>10} "
f"{cp['nhhi']:>9.1f}{cp['neff']:>9.1f}{sh_xsr_star:>8.2f}{1.0:>9.0%}")
print(" 'Sh MED' = mediana delle 10 fasi d'ancora di XS01 = la stima ONESTA (la fase 0 e' un")
print(" max-of-10 non dichiarato). XSR01 e' giornaliero: non ha fase, la sua colonna e' unica.")
print(" N_hhi = gambe che PORTANO il rischio (stabile). N_eff = formula pubblicata, esplode")
print(" quando rbar e' negativo: si legge solo insieme a rbar, mai da sola.")
# ---------------------------------------------------------------- (4) banda d'ancora
print("\n" + "-" * 108)
print(" (4) BANDA D'ANCORA — le 10 fasi del ciclo H=10 di XS01 (XSR01 e' giornaliero: nessuna")
print(" fase). Stima onesta = MEDIANA della banda, non la fase 0 con cui e' stato scoperto.")
print("-" * 108)
print(f" {'cella':<14}{'fase0':>8}{'mediana':>9}{'p10':>8}{'p90':>8}{'pctl f0':>9}"
f" d vs k=5: mediana APPAIATA (fasi >0)")
for k in KS:
vals, diffs = phase_rows[k]
pctl = float((vals <= vals[0]).mean() * 100)
print(f" MAJ19 k={k:<6}{vals[0]:>8.2f}{phase_med[k]:>9.2f}{np.percentile(vals,10):>8.2f}"
f"{np.percentile(vals,90):>8.2f}{pctl:>8.0f}% {np.median(diffs):>+8.3f}"
f" {int((diffs>0).sum())}/10")
print(" 'd vs k=5' = mediana delle DIFFERENZE APPAIATE per fase (mai differenza di mediane).")
# ---------------------------------------------------------------- (5) eseguibilita'
print("\n" + "-" * 108)
print(" (5) ESEGUIBILITA' — contabilita' min-order per gamba, lente 'libro'")
print(" Il capitale in colonna e' quello dello SLEEVE. XS01 sta al 15% del book:")
print(" sleeve $90 = book $600, sleeve $300 = book $2.000, sleeve $750 = book $5.000,")
print(" sleeve $3.000 = book $20.000 (la soglia dichiarata dal progetto).")
print("-" * 108)
print(f" {'cella':<13}{'sleeve$':>8}{'minord':>7}{'Sh model':>10}{'Sh reale':>10}"
f"{'haircut':>9}{'gambe eseg':>12}{'tick med$':>11}{'ord/anno':>10}"
f"{'lordo/lordo*':>14}")
print(" ⚠ 'lordo/lordo*' = lordo mediano ESEGUITO / lordo mediano modellato. Sotto ~0.8 il")
print(" libro ha smesso di seguire il proprio target, e allora lo Sharpe NON e' la lettura")
print(" giusta (puo' perfino MIGLIORARE): e' il null del de-levering in veste di 'meno")
print(" costi'. Lezione GTAA-banda 27/07, riapplicata qui.")
def xs_book_inputs(nm):
cell = xs_cells[nm]
return xs_effective_weights(cell["W"], cell["scale"]), cell["dret"], cell["C"].index, None
def xsr_book_inputs(nm):
cell = xsr_cells[nm]
Q, Su, norm = cell["Q"], cell["S"], cell["norm"]
Wt = np.nan_to_num(Q.to_numpy(float)) / float(norm)
Weff = np.zeros_like(Wt); Weff[1:] = Wt[:-1]
rb = load_hl(BASE).reindex(Q.index).pct_change().fillna(0.0).to_numpy(float)
Ralt = np.zeros_like(Wt)
for j, c in enumerate(Q.columns):
Ralt[:, j] = np.nan_to_num(Su[c].to_numpy(float)) + rb
return Weff, Ralt, Q.index, rb
for tag, names, getter in (("XS", ["MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"],
xs_book_inputs),
("XSR", ["ALL50 k=1", "ALL50 k=2", "ALL50 k=3", "ALL50 k=pieno*"],
xsr_book_inputs)):
for nm in names:
Weff, R, idx, rh = getter(nm)
base, dg0 = sim_book(Weff, R, idx, r_hedge=rh, capital=None, min_order=0.0)
for cap in SLEEVE_CAPS:
for mo in (MIN_ORDER_HL,):
real, dg = sim_book(Weff, R, idx, r_hedge=rh, capital=cap, min_order=mo)
lbl = f"{tag} {nm.split()[1]}"
trk = (dg["med_gross"] / dg0["med_gross"]) if dg0["med_gross"] > 0 else float("nan")
print(f" {lbl:<13}{cap:>8.0f}{mo:>7.0f}{sh(base):>10.2f}{sh(real):>10.2f}"
f"{sh(base)-sh(real):>9.2f}{dg['exec_share']*100:>11.0f}%"
f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}"
f"{trk:>12.2f}{' ⚠' if trk < 0.8 else ' '}")
print()
print(f" {'cella':<13}{'|dw| RIBILANCIO':>18}{'|dw| deriva':>14}{'cap sleeve min':>17}"
f"{'= book al 15%':>16}")
capmin_k5 = None
for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"):
cell = xs_cells[nm]
W = cell["W"]
Weff = xs_effective_weights(W, cell["scale"])
dW = np.abs(np.diff(W, axis=0)) # W cambia alla riga j+1
dE = np.abs(np.diff(Weff, axis=0)) # Weff[j+1]-Weff[j], Weff[i]=W[i-1]*scale[i]
# il cambio di W alla riga j+1 si vede su Weff alla riga j+2 -> confine da rispettare
rb, dr = [], []
for j in range(len(dW)):
if j + 1 >= len(dE):
break
m = dW[j] > 1e-12
if m.any():
rb.extend(dE[j + 1][m].tolist()) # gambe DAVVERO ribilanciate
dr.extend(dE[j + 1][~m][dE[j + 1][~m] > 1e-12].tolist())
else:
dr.extend(dE[j + 1][dE[j + 1] > 1e-12].tolist())
med_rb = float(np.median(rb)) if rb else float("nan")
med_dr = float(np.median(dr)) if dr else float("nan")
capmin = MIN_ORDER_HL / med_rb if med_rb > 0 else float("nan")
if nm.startswith("MAJ19 k=5"):
capmin_k5 = capmin
print(f" {'XS ' + nm.split()[1]:<13}{med_rb:>18.4f}{med_dr:>14.4f}{capmin:>16.0f}$"
f"{capmin/0.15:>15.0f}$")
print(" ^ ARITMETICA: il ticket vale |dw|*capitale_sleeve, e le due colonne |dw| sono ordini")
print(" di grandezza diversi: il RIBILANCIO (ogni 10 giorni, porta il segnale) e la DERIVA")
print(" giornaliera del vol-target (non porta segnale). Il min-order taglia la seconda molto")
print(" prima della prima, ed e' per questo che la quota di turnover eseguito puo' essere del")
print(f" 4% con haircut ~0. Per il CANONICO k=5 il ribilancio smette di passare sotto "
f"~{capmin_k5:.0f}$")
print(f" di sleeve = ~{capmin_k5/0.15:.0f}$ di book al 15%. La soglia '$20k' del progetto viene da")
print(" una diagnostica di TURNOVER, non di Sharpe — stessa lezione gia' imparata su TP01")
print(" ('a $600 il min-order $5 e' GIA' la banda ottimale'), mai applicata a XS01.")
print()
print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica")
print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose")
print(" DIVERSE: il turnover saltato e' la deriva del vol-target, non il segnale.")
print(" Con min-order $5 invece di $10 (Deribit invece di HL) l'haircut si dimezza per")
print(" costruzione; il caso $10 e' quello vero per Hyperliquid ed e' quello riportato.")
# ---------------------------------------------------------------- (6) slippage
print("\n" + "-" * 108)
print(" (6) SLIPPAGE — costo TOTALE per gamba (i 5 bps di fee taker sono gia' dentro).")
print(" A che costo muore l'edge? E' il rischio #1 dichiarato di XSR01.")
print("-" * 108)
print(f" {'cella':<16}" + "".join(f"{int(c):>10}bps" for c in COST_SWEEP) + f"{'break-even':>13}")
def cost_curve(label, Weff, R, idx, rh):
vals = []
for c in COST_SWEEP:
s, _ = sim_book(Weff, R, idx, r_hedge=rh, capital=None, min_order=0.0, cost_bps=c)
vals.append(sh(s))
be = None
for i in range(1, len(COST_SWEEP)):
if vals[i - 1] > 0 >= vals[i]:
x0, x1, y0, y1 = COST_SWEEP[i-1], COST_SWEEP[i], vals[i-1], vals[i]
be = x0 + (x1 - x0) * y0 / (y0 - y1)
break
print(f" {label:<16}" + "".join(f"{v:>13.2f}" for v in vals)
+ (f"{be:>13.0f}" if be else f"{'>'+str(int(COST_SWEEP[-1])):>13}"))
return be
be = {}
for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"):
be["XS " + nm] = cost_curve("XS " + nm.split()[1], *xs_book_inputs(nm))
for nm in ("ALL50 k=1", "ALL50 k=2", "ALL50 k=3", "ALL50 k=pieno*"):
be["XSR " + nm] = cost_curve("XSR " + nm.split()[1], *xsr_book_inputs(nm))
# ---------------------------------------------------------------- (7) selezione + DSR
print("\n" + "-" * 108)
print(" (7) SELEZIONE IN-SAMPLE-ONLY (mai sull'hold-out) + DEFLATED SHARPE")
print("-" * 108)
allcells = {**{f"XS {n}": s for n, s in xs_series.items()},
**{f"XSR {n}": s for n, s in xsr_series.items()}}
ins_rank = sorted(allcells.items(), key=lambda kv: -sh(split(kv[1])[0]))
print(f" celle valutate: {len(allcells)} (2 famiglie x 2 universi x 6 k, + i 2 'pieno' di")
print(" XSR01, meno le celle impossibili 2k>gambe). Lookback NON riaperto -> nessun trial in piu'.")
print(f"\n {'rank':>5} {'cella':<20}{'Sh IS':>8}{'Sh OOS':>8}{'Sh FULL':>9}")
for i, (n, s) in enumerate(ins_rank[:8], 1):
ins, out = split(s)
print(f" {i:>5} {n:<20}{sh(ins):>8.2f}{sh(out):>8.2f}{sh(s):>9.2f}")
best_name, best_s = ins_rank[0]
all_sr = [sh(s) for s in allcells.values()]
print(f"\n cella scelta AL BUIO (solo in-sample): {best_name}")
for mult, label in ((1, "trial dichiarati = celle"),
(6, "conteggio conservativo x6 (se avessi riaperto il lookback)")):
pad = all_sr * mult
d, s0 = A.deflated_sharpe(sh(best_s), pad, pd.Series(best_s).dropna())
print(f" DSR a N={len(pad):<4} ({label}): {d:.3f} "
f"{'PASS' if d >= 0.95 else 'FAIL'} (Sharpe-soglia del null {s0:.2f})")
conc = {n: s for n, s in allcells.items() if any(f" k={j}" in n for j in (1, 2, 3))}
cbest_name, cbest_s = max(conc.items(), key=lambda kv: sh(split(kv[1])[0]))
d2, _ = A.deflated_sharpe(sh(cbest_s), all_sr, pd.Series(cbest_s).dropna())
print(f" cella CONCENTRATA (k<=3, cioe' <=6 gambe) scelta al buio: {cbest_name} "
f"Sh IS {sh(split(cbest_s)[0]):.2f} OOS {sh(split(cbest_s)[1]):.2f} FULL {sh(cbest_s):.2f}")
print(f" DSR della concentrata a N={len(all_sr)}: {d2:.3f} {'PASS' if d2 >= 0.95 else 'FAIL'}")
# ---------------------------------------------------------------- (8) gate marginali
print("\n" + "-" * 108)
print(" (8) GATE MARGINALE vs TP01 + RIDONDANZA vs gli sleeve che gia' esistono")
print("-" * 108)
order = [best_name, cbest_name, "XS MAJ19 k=2", "XS MAJ19 k=3", "XS MAJ19 k=5*",
"XSR ALL50 k=2", "XSR ALL50 k=3", "XSR ALL50 k=pieno*"]
to_score = {}
for n in order:
if n in allcells and n not in to_score:
to_score[n] = allcells[n]
for n, s in to_score.items():
ss = pd.Series(s).dropna(); ss.index = pd.to_datetime(ss.index, utc=True)
m = A.marginal_vs_tp01(ss)
b = m.get("blends", {}).get("w25", {})
print(f" {n:<20} {str(m.get('marginal_verdict')):<10} corr {str(m.get('corr_full')):>7}"
f" uplift w25 full {str(b.get('uplift_full')):>7} hold {str(b.get('uplift_hold')):>7}"
f" robust={m.get('robust_oos')} noise_null={m.get('beats_noise_null')}"
f" is_edge={m.get('has_insample_edge')} hedge={m.get('is_hedge')}")
print("\n correlazione col materiale gia' in casa (un proxy eseguibile DEVE assomigliare al suo")
print(" originale: qui una corr ALTA e' l'obiettivo, non un difetto)")
ref_map = {"XS01 (sleeve)": ref, "XSR01 (pieno)": xsr_const, "TP01": A.tp01_baseline_daily()}
print(f" {'cella':<20}" + "".join(f"{k:>16}" for k in ref_map))
for n, s in to_score.items():
line = f" {n:<20}"
for _, r in ref_map.items():
Jc = pd.concat({"a": pd.Series(s).dropna(), "b": r}, axis=1, join="inner").dropna()
line += f"{(Jc['a'].corr(Jc['b']) if len(Jc) > 30 else float('nan')):>16.3f}"
print(line)
print(" ⚠ corr a SKH01/VRP01/GTAA01 NON misurata: SKH01 richiede i 5m di DUE asset e il brief")
print(" vieta di caricarli insieme. Prior del progetto (25/07): XSR01 sta a |0.001|-|0.071|")
print(" da tutti e 5 gli sleeve; un suo sottoinsieme non puo' esserne lontano.")
# ---------------------------------------------------------------- (9) null del de-levering
print("\n" + "-" * 108)
print(" (9) NULL DEL DE-LEVERING — obbligatorio su ogni confronto di maxDD (5 occorrenze note)")
print(" Domanda: esiste una LEVA del canonico che dia lo STESSO maxDD della variante?")
print(" Se a quel maxDD il canonico ha Sharpe >= alla variante, la variante non aggiunge.")
print("-" * 108)
def lev_for_dd(canon: pd.Series, target_abs: float, lo=0.01, hi=5.0):
"""Leva k tale che |maxDD(canon*k)| = target. |maxDD| e' monotono crescente in k."""
if abs(dd(canon * hi)) < target_abs:
return None # nemmeno a leva 5x si arriva al DD
for _ in range(60):
mid = 0.5 * (lo + hi)
if abs(dd(canon * mid)) < target_abs: lo = mid
else: hi = mid
return 0.5 * (lo + hi)
def delever_null(variants: dict, canon: pd.Series, label: str):
print(f" {label:<22}{'maxDD':>9}{'Sh':>8} {'k_leva canonico':>16}{'Sh canonico':>13} esito")
for nm, s in variants.items():
kl = lev_for_dd(canon, abs(dd(s)))
if kl is None:
print(f" {nm:<22}{dd(s)*100:>8.1f}%{sh(s):>8.2f} {'n.d. (>5x)':>16}"
f"{'':>13} maxDD irraggiungibile levando il canonico")
continue
sc_ = sh(canon * kl)
print(f" {nm:<22}{dd(s)*100:>8.1f}%{sh(s):>8.2f} {kl:>16.3f}{sc_:>13.2f} "
f"{'il canonico DOMINA' if sc_ >= sh(s) else 'la variante REGGE'}")
delever_null({n: xs_series[n] for n in xs_series if n.startswith("MAJ19") and "k=5" not in n},
xs_series["MAJ19 k=5*"], "XS-LITE vs canonico")
print()
delever_null({n: xsr_series[n] for n in xsr_series if n.startswith("ALL50") and "pieno" not in n},
xsr_series["ALL50 k=pieno*"], "XSR-LITE vs pieno")
# ---------------------------------------------------------------- (10) implausible + causalita'
print("\n" + "-" * 108)
print(" (10) implausible_sharpe + CAUSALITA' (prefisso troncato + decadimento con lag)")
print("-" * 108)
for n in dict.fromkeys([best_name, cbest_name, "XS MAJ19 k=2", "XS MAJ19 k=5*",
"XSR ALL50 k=2"]):
if n not in allcells:
continue
ss = pd.Series(allcells[n]).dropna(); ss.index = pd.to_datetime(ss.index, utc=True)
r = A.implausible_sharpe(ss)
print(f" {n:<20} implausible={str(r['implausible']):<6} Sh {r.get('sharpe',0):>5.2f} "
f"perdite su barre ATTIVE {r.get('loss_frac',float('nan'))*100:>5.1f}% "
f"attive {r.get('active_frac',0)*100:>3.0f}% {'; '.join(r['reasons'])[:38]}")
cut = int(len(C19) * 0.85)
Wf = xs_run(C19, k=2, phase=0)[1]
Wp = xs_run(C19.iloc[:cut], k=2, phase=0)[1]
dmax = float(np.max(np.abs(Wp[cut - 80:cut] - Wf[cut - 80:cut])))
print(f" XS k=2: pesi ricalcolati su prefisso troncato (cut={cut}) vs pieno, ultime 80 barre: "
f"max|diff| = {dmax:.2e} {'CAUSALE' if dmax < 1e-12 else 'LOOK-AHEAD'}")
Wq = xs_run(C19, k=2, phase=0)
Weff2 = xs_effective_weights(Wq[1], Wq[2])
lags = []
for lag in (0, 1, 2, 3, 5):
Wl = np.zeros_like(Weff2)
if lag: Wl[lag:] = Weff2[:-lag]
else: Wl = Weff2
s, _ = sim_book(Wl, Wq[3], C19.index)
lags.append(sh(s))
print(" XS k=2 decadimento con lag (0/1/2/3/5 giorni): "
+ " / ".join(f"{v:.2f}" for v in lags)
+ " (dolce = segnale lento, non firma di look-ahead)")
# ---------------------------------------------------------------- (11) sintesi
print("\n" + "=" * 108)
print(" SINTESI — quanto costa concentrare, e dove sta davvero il muro")
print("=" * 108)
k2, k5 = xs_series["MAJ19 k=2"], xs_series["MAJ19 k=5*"]
print(f" XS-LITE k=2 (4 gambe): Sh fase0 {sh(k2):.2f} / fase-MEDIANA {phase_med[2]:.2f}"
f" vs canonico k=5 (10 gambe): {sh(k5):.2f} / {phase_med[5]:.2f}")
print(f" XSR-LITE k=2 (4 gambe): Sh {sh(xsr_series['ALL50 k=2']):.2f} "
f"(IS {sh(split(xsr_series['ALL50 k=2'])[0]):.2f}) vs pieno "
f"{sh_xsr_star:.2f} (IS {sh(split(xsr_series['ALL50 k=pieno*'])[0]):.2f})")
def _be(x):
return f"{x:.0f} bps" if x else f">{int(COST_SWEEP[-1])} bps"
print(f" break-even di costo per gamba: XS k=2 {_be(be.get('XS MAJ19 k=2'))}, "
f"XS k=5 {_be(be.get('XS MAJ19 k=5*'))}, XSR k=2 {_be(be.get('XSR ALL50 k=2'))}, "
f"XSR pieno {_be(be.get('XSR ALL50 k=pieno*'))}")
print("-" * 108)
print(" 1. CONCENTRARE NON COMPRA RENDIMENTO, COMPRA ESEGUIBILITA'. Il null del de-levering")
print(" boccia ogni k tranne i due che sono fortuna d'ancora (XS k=4: fase0 al 90° pctl,")
print(" mediana 1.10 < 1.15 del canonico; XSR k=6: +0.03 su 1.65). Nessuna concentrazione")
print(" batte il proprio originale a PARI maxDD.")
print(" 2. MA IL COSTO E' MOLTO PIU' BASSO DEL PREVISTO, e la mia ipotesi (a) era sbagliata:")
print(" mi aspettavo Sh ~ sqrt(ampiezza) e quindi il 40-60% dell'edge a k=2-3. Misurato:")
print(" XS-LITE k=2 conserva il 98% del canonico A MEDIANA DI FASE con 4 gambe invece di 10,")
print(" e N_hhi scende 6.9 -> 3.0. La radice dell'ampiezza NON descrive questa curva: la")
print(" dispersione cross-section e' concentrata negli ESTREMI del ranking, e le gambe 3-5")
print(" per lato aggiungono rischio quanto segnale. Il collasso vero e' solo a k=1 (41%).")
print(" 3. IL MURO NON E' IL MIN-ORDER, E' IL PESO DELLO SLEEVE. Con $600 DEDICATI, XS01 gira")
print(" gia' oggi a k=5 (tracking 1.04, haircut -0.04). E' al 15% del book che muore: a")
print(" sleeve $90 il canonico esegue il 4% delle variazioni e il lordo scende al 13% del")
print(" target. A quel capitale k=2 e' l'unica cella CON EDGE che traccia (1.01: k=1 traccia")
print(" ma non ha edge, Sh 0.13). Il '~$20k' e'")
print(" quindi il prodotto di due scelte (peso 15% + k=5), non una proprieta' del segnale.")
print(" 4. XSR01 NON SI CONCENTRA: k<=3 perde l'edge IN-SAMPLE (IS 0.96 -> 0.28) e il gate")
print(" marginale lo declassa ad ADDS -> NOISE (has_insample_edge=False). Ipotesi (c)")
print(" CONFERMATA: li' l'ampiezza E' l'edge. E muore a ~28 bps/gamba di costo totale a")
print(" OGNI k, contro >50 bps di XS-LITE: concentrare non ripara il rischio #1 di XSR01.")
print(" 5. ⛔ NIENTE DI QUESTO E' DEPLOYABILE OGGI, e non per una ragione misurata qui:")
print(" XS01/XSR01 vivono su HYPERLIQUID, e la decisione dell'operatore del 26/07 tiene")
print(" il 100% del capitale su Deribit fino a $20k. A $20k il canonico e' eseguibile da")
print(" solo. XS-LITE serve SE e QUANDO si apre HL sotto quella soglia — non prima.")
print("-" * 108)
print(" GATE PRE-REGISTRATO (dichiarato ORA, prima di qualunque dato nuovo):")
print(" XS-LITE k=2 su MAJ19 si riapre SOLO se si apre un conto Hyperliquid con >= $300")
print(" dedicati allo sleeve, e in quel caso si deploya il k CANONICO (5) se il capitale")
print(" dello sleeve e' >= $750, k=2 sotto. Nessun nuovo backtest serve per questa scelta:")
print(" e' gia' decisa dalla tabella (5) e non va ri-ottimizzata guardando i dati nuovi.")
print(" Come CANDIDATO a sleeve XS-LITE resta bocciato: DSR FAIL a ogni conteggio e null")
print(" del de-levering non superato. Non si ri-testa prima del 2027-06 (3.5 anni di")
print(" storia HL, di cui >=1.5 fuori dal campione di scoperta di XS01).")
print("=" * 108)
if __name__ == "__main__":
main()
+433
View File
@@ -0,0 +1,433 @@
"""XSR-REPRO — lo Sharpe 1.82 di XSR01 non si riproduce: e' un problema di CODICE, di DATI, o di LENTE?
PERCHE'. XSR01 ha un gate di deploy PRE-REGISTRATO al 2026-10-23 (`r0725_xsr_deploy_gate.py`):
deploy solo se Sharpe forward >= 1.0 E haircut di eseguibilita' a $5.000 <= 40%. I suoi numeri di
ammissione — Sharpe netta 1.82, lorda 2.70, maxDD -2.6%, vol 2.3%, ret 4.2%/a, lag 1.82/1.19/0.81/
0.51, Sharpe per anno 1.03/1.98/3.11 — sono in CLAUDE.md, nel diario del 25/07 e CABLATI nel gate
(`IS_SHARPE_NET = 1.82`). Il filone XS-LITE (22/08) ha misurato che sulla finestra identica a quella
di scoperta oggi escono 1.75 (lente "paniere") e 2.23 (lente "libro"): 1.82 non e' nessuna delle due.
Una decisione fra due mesi poggia su un numero che non si riproduce.
Questo script NON giudica XSR01 e NON anticipa il gate del 23/10 (sarebbe selection-on-forward).
Stabilisce SOLO se il numero su cui si decidera' e' solido, in quest'ordine:
T1 QUALE LENTE. Tre implementazioni convivono nel repo e danno numeri diversi sugli STESSI dati:
L1 `r0725_statarb_basket_gate.basket_from_positions(demean=True)` — media per riga sulle sole
colonne NON-NaN (divisore variabile). E' la lente che ha girato i gate (marginale, DSR).
L2 `r0725_statarb_demean_skeptic.ret_from_pos` — P.fillna(0) e divisore FISSO 50.
L3 `scripts/live/paper_xsr.build_panel` + `_step` — libro con capitale, pesi Q/50, gamba BTC
esplicita. E' quella che alimenta il gate.
T2 QUALE STATO DEL DATO. `data/raw` e' gitignored e il cron riscrive i parquet HL ogni notte.
Ipotesi da falsificare: "stesso codice, dati diversi" (identico allo scoperto GTAA/TLT 07/08).
Prove indipendenti: (a) il log del cron registra `reali`/`bfill`/`start_reale` per simbolo a
ogni giro -> se il conteggio delle barre PASSATE e' cambiato, il feed e' stato riscritto;
(b) lo Sharpe per ANNO CHIUSO (2024, 2025) e' un'impronta della storia: se la storia e'
stabile deve riprodursi al centesimo attraverso 4 settimane di riscritture.
T3 IL MONITOR FORWARD. `paper_xsr` e' forward-only e append-only, quindi in teoria immune alla
riscrittura. Test diretto: replay del libro MODELED sui dati di OGGI dall'inception, barra per
barra, contro cio' che il monitor ha REGISTRATO giorno per giorno. Se la storia fosse stabile
e la lente giusta, le due serie devono coincidere.
T4 L'HAIRCUT A $5.000, l'altra meta' del gate: dipende dal min-order del venue. Il numero
pubblicato ($14.41 di ticket, soglia 40%) assume min-order $5 (Deribit). Hyperliquid potrebbe
essere $10. Si misura la sensibilita'.
COSA MI ASPETTAVO PRIMA DI MISURARE: che fosse la lente (tre implementazioni, nessuna nominata nel
diario) e che il dato fosse stabile, perche' il log del cron mostra bar-count che crescono di 1/g.
Solo la prima meta' e' risultata vera.
nice -n 19 timeout 900 uv run python scripts/research/r0822_xsr_repro.py
"""
from __future__ import annotations
import json
import re
import sys
from collections import OrderedDict
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(_p))
from r0725_statarb_multi import BASE, MIN_BARS, _dd, _sh, load_hl, signal, universe # noqa: E402
from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402
from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402
ANN = np.sqrt(365.0)
DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta (PROVVISORIA)
CLOSED_AT_DISCOVERY = pd.Timestamp("2026-07-24", tz="UTC") # ultima barra CHIUSA quel giorno
CRONLOG = ROOT / "logs" / "cron_daily.log"
FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl"
FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json"
# I NUMERI PUBBLICATI (CLAUDE.md + diario 2026-07-25 + docstring di paper_xsr / del gate)
PUB = dict(netta=1.82, lorda=2.70, dd=-2.6, vol=2.3, ret=4.2,
lag=(1.82, 1.19, 0.81, 0.51), anni=(1.03, 1.98, 3.11))
def _cut(df, ts):
return df[df.index <= ts]
def _stats(r: pd.Series) -> dict:
return dict(n=len(r), sh=_sh(r.values), dd=_dd(r.values) * 100,
vol=float(r.std() * ANN * 100), ret=float(r.mean() * 365 * 100))
# ------------------------------------------------------------------ pannelli
def panel(partial_last: bool = False, cut: pd.Timestamp | None = None):
"""(P, S) come `pair_frames`, ma con due gradi di liberta' espliciti:
`cut` -> ultima barra inclusa (troncamento dell'INPUT, non dell'output);
`partial_last` -> l'ultima barra e' resa PARZIALE (close := open), cioe' com'era il giorno in
cui il cron l'aveva appena scritta. E' l'unico stato del dato che questo
script NON puo' leggere da disco: va ricostruito."""
def px(sym):
d = pd.read_parquet(RAW / f"hl_{sym.lower()}_1d.parquet")
ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
c = pd.Series(d["close"].astype(float).values, index=ix).sort_index()
o = pd.Series(d["open"].astype(float).values, index=ix).sort_index()
c = c[~c.index.duplicated(keep="last")]
o = o[~o.index.duplicated(keep="last")]
if cut is not None:
c, o = c[c.index <= cut], o[o.index <= cut]
if partial_last and len(c):
c.iloc[-1] = o.iloc[-1]
return c
base = px(BASE)
pc, sc = {}, {}
for sym in universe():
try:
tgt = px(sym)
except FileNotFoundError:
continue
ix = base.index.intersection(tgt.index)
if len(ix) < MIN_BARS:
continue
p, s = signal(base[ix], tgt[ix])
pc[sym] = pd.Series(p, index=ix)
sc[sym] = pd.Series(s, index=ix)
P = pd.concat(pc, axis=1, sort=True).sort_index()
S = pd.concat(sc, axis=1, sort=True).sort_index()
return P, S
def lens_L1(P, S):
return basket_from_positions(P, S, demean=True)
def lens_L2(P, S, fee_leg=0.0005, lag=0):
P0 = P.fillna(0.0)
S0 = S.reindex(P0.index)[P0.columns]
return ret_from_pos(P0, S0, fee_leg=fee_leg, lag=lag)
# ------------------------------------------------------------------ T1
def t1_lenti():
print("=" * 100)
print(" T1 — QUALE LENTE, E QUALE STATO DEL DATO, PRODUCE 1.82")
print("=" * 100)
P, S = pair_frames()
print(f"\n dato su disco oggi: {P.shape[1]} coppie, {len(P)} barre "
f"({P.index[0].date()} -> {P.index[-1].date()})")
rows = []
for tag, (Pi, Si) in [
("OGGI pieno", (P, S)),
(f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))),
("tronc. 2026-07-24 = ultima barra CHIUSA il 25/07",
(_cut(P, CLOSED_AT_DISCOVERY), _cut(S, CLOSED_AT_DISCOVERY))),
]:
rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si))))
rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si))))
Pp, Sp = panel(partial_last=True, cut=DISCOVERY)
rows.append(("L1 gate STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L1(Pp, Sp))))
rows.append(("L2 scett. STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L2(Pp, Sp))))
print(f"\n {'lente / stato del dato':<52}{'n':>6}{'Sharpe':>9}{'maxDD':>9}{'vol':>8}{'ret/a':>8}")
for nm, s in rows:
print(f" {nm:<52}{s['n']:>6}{s['sh']:>9.4f}{s['dd']:>8.2f}%{s['vol']:>7.2f}%{s['ret']:>7.2f}%")
print(f" {'--- PUBBLICATO 2026-07-25':<52}{'937?':>6}{PUB['netta']:>9.2f}"
f"{PUB['dd']:>8.1f}%{PUB['vol']:>7.1f}%{PUB['ret']:>7.1f}%")
# confronto completo sugli OTTO numeri pubblicati, per la sola combinazione che li riproduce
print("\n --- verifica completa sullo stato che riproduce: L2 + ultima barra PARZIALE ---")
r = lens_L2(Pp, Sp)
g = lens_L2(Pp, Sp, fee_leg=0.0)
lags = tuple(_sh(lens_L2(Pp, Sp, lag=L).values) for L in (0, 1, 2, 3))
anni = tuple(_sh(r[r.index.year == y].values) for y in (2024, 2025, 2026))
st = _stats(r)
ok = 0
def chk(nome, mio, pub, tol):
nonlocal ok
good = abs(mio - pub) <= tol
ok += good
print(f" {nome:<22} misurato {mio:>8.4f} pubblicato {pub:>7.2f} "
f"{'COINCIDE' if good else 'DIVERGE'}")
chk("Sharpe netta", st["sh"], PUB["netta"], 0.005)
chk("Sharpe lorda", _sh(g.values), PUB["lorda"], 0.005)
chk("maxDD %", st["dd"], PUB["dd"], 0.05)
chk("vol ann %", st["vol"], PUB["vol"], 0.05)
chk("ret ann %", st["ret"], PUB["ret"], 0.05)
for i, L in enumerate((0, 1, 2, 3)):
chk(f"lag +{L}g", lags[i], PUB["lag"][i], 0.006)
for i, y in enumerate((2024, 2025, 2026)):
chk(f"Sharpe {y}", anni[i], PUB["anni"][i], 0.005)
print(f"\n -> {ok}/12 numeri pubblicati riprodotti da L2 sullo stato del dato del 25/07.")
# la lente che ha girato i GATE non e' quella del titolo
print("\n --- e la lente che ha girato i gate (marginale, deflated-Sharpe) e' L1, non L2 ---")
print(f" L1 sullo stato del 25/07 : Sharpe {_sh(lens_L1(Pp, Sp).values):.4f}")
print(" docstring di r0725_statarb_demean_skeptic.py, riga 5, cita il gate come "
"\"Sharpe 1.79\"")
print(" -> il numero di TITOLO (1.82, L2) e i numeri di GATE (DSR/marginale, L1) non sono")
print(" mai stati la stessa serie.")
# deflated-Sharpe delle 3 varianti pre-registrate, oggi, sulle due lenti
try:
from altlib import deflated_sharpe
from src.portfolio.sleeves import XS_UNIVERSE
maj = [s for s in XS_UNIVERSE if s != BASE]
Pc, Sc = _cut(P, DISCOVERY), _cut(S, DISCOVERY)
var = {"V1 ALL50": dict(cols=None, demean=False),
"V2 MAJ19": dict(cols=maj, demean=False),
"V3 DEMEAN": dict(cols=None, demean=True)}
ser = {k: basket_from_positions(Pc, Sc, **v) for k, v in var.items()}
srs = [_sh(v.values) for v in ser.values()]
print("\n --- deflated-Sharpe delle 3 varianti pre-registrate, L1, finestra 25/07 ---")
for k, v in ser.items():
d, nm = deflated_sharpe(_sh(v.values), srs, v.values, dpy=365.0)
print(f" {k:<11} Sharpe {_sh(v.values):>5.2f} DSR {d:>6.3f} "
f"(max atteso dal null {nm:>5.2f}) {'PASS' if d >= 0.95 else 'SOTTO 0.95'}")
print(" (pubblicato per V3: DSR 0.985)")
except Exception as e: # pragma: no cover
print(f" [DSR non calcolabile: {e.__class__.__name__}: {e}]")
return P, S
# ------------------------------------------------------------------ T2
def t2_feed(P, S):
print("\n" + "=" * 100)
print(" T2 — IL FEED HL E' STATO RISCRITTO? (l'ipotesi principale: stesso codice, dati diversi)")
print("=" * 100)
# (a) il log del cron registra reali/bfill/start_reale a ogni giro: e' un registro storico
runs, cur, inblk, hdr = OrderedDict(), None, False, None
for ln in CRONLOG.read_text(errors="replace").splitlines():
m = re.match(r"^===== (\S+) cron_daily =====", ln)
if m:
cur, inblk = m.group(1), False
runs[cur] = {}
continue
if "FETCH + CERTIFY Hyperliquid" in ln:
inblk, hdr = True, None
continue
if inblk and cur is not None:
if ln.strip().startswith("sym"):
hdr = ln.split()
continue
f = ln.split()
if hdr and len(f) >= 7 and re.match(r"^[A-Z0-9]+$", f[0]) and f[1].isdigit():
runs[cur][f[0]] = ((int(f[1]), f[3], f[-1]) if len(hdr) >= 9
else (int(f[1]), f[2], f[-1]))
elif runs[cur] and (not ln.strip() or ln.startswith("=")):
inblk = False
keys = [k for k, v in runs.items() if v]
a = next((k for k in keys if k.startswith("2026-07-25")), None)
b = keys[-1] if keys else None
print(f"\n (a) registro del cron: {len(keys)} giri con blocco Hyperliquid, "
f"da {keys[0][:10]} a {keys[-1][:10]}")
if a and b:
A, B = runs[a], runs[b]
atteso = (pd.Timestamp(b[:10]) - pd.Timestamp(a[:10])).days
anom = []
for s in sorted(set(A) | set(B)):
ra, rb = A.get(s), B.get(s)
if ra is None or rb is None:
anom.append((s, "presente in un solo giro", ra, rb)); continue
if rb[0] - ra[0] != atteso or ra[1] != rb[1]:
anom.append((s, f"delta {rb[0]-ra[0]} invece di {atteso}; start {ra[1]}->{rb[1]}",
ra, rb))
print(f" confronto {a[:10]} vs {b[:10]} su {len(set(A)|set(B))} simboli, "
f"crescita attesa {atteso} barre")
if not anom:
print(" -> TUTTI i simboli crescono di esattamente il numero di giorni e "
"start_reale INVARIATO")
for s, why, ra, rb in anom:
print(f" {s:<8} {why} (verdetto oggi: {rb[2] if rb else '-'})")
print(" NB: i simboli con delta 0 sono quelli con verdetto `scarta` (serie ferma sul")
print(" venue): NON producono un parquet, quindi non entrano nell'universo.")
# (b) impronta indipendente della storia: lo Sharpe degli ANNI CHIUSI
Pp, Sp = panel(partial_last=True, cut=DISCOVERY)
r_then = lens_L2(Pp, Sp)
r_now = lens_L2(P, S)
print("\n (b) impronta della storia — Sharpe degli anni CHIUSI, che una riscrittura sposterebbe:")
print(f" {'anno':<8}{'stato 25/07':>14}{'stato oggi':>13}{'pubblicato':>13}{'barre':>8}")
for y, pub in zip((2024, 2025, 2026), PUB["anni"]):
s1 = r_then[r_then.index.year == y]
s2 = r_now[r_now.index.year == y]
note = " <- anno IN CORSO: cambia perche' contiene barre nuove" if y == 2026 else ""
print(f" {y:<8}{_sh(s1.values):>14.4f}{_sh(s2.values):>13.4f}{pub:>13.2f}"
f"{len(s2):>8}{note}")
print(" -> 2024 e 2025 (731 barre) si riproducono al centesimo attraverso 4 settimane di")
print(" riscritture notturne: la STORIA CHIUSA e' stabile.")
# (c) l'unica barra che cambia: l'ultima
d = pd.read_parquet(RAW / "hl_btc_1d.parquet")
vol = d["volume"].astype(float).values
q = float(vol[-1] / np.median(vol[-40:-1]))
print("\n (c) l'unica barra che NON e' definitiva e' l'ULTIMA. `fetch_hyperliquid` gira nel")
print(" cron delle 00:30 con END = oggi, quindi scrive la barra del giorno IN CORSO:")
print(f" hl_btc_1d ultima barra {pd.Timestamp(int(d['timestamp'].iloc[-1]), unit='ms', tz='UTC')}"
f" volume {vol[-1]:,.0f} = {q*100:.1f}% del volume mediano di un giorno pieno")
print(f" (una barra piena di ~24h; questa ne ha ~{q*24*60:.0f} minuti di scambi)")
print("\n VERDETTO T2: il feed VIENE riscritto, ma NON come ipotizzato. Non c'e' riscrittura")
print(" arbitraria della storia: c'e' UNA barra provvisoria per volta — l'ultima — che il giro")
print(" successivo completa. Ogni numero calcolato su dato HL il giorno D contiene una barra")
print(" di ~30-40 minuti travestita da giorno.")
# ------------------------------------------------------------------ T3
def t3_monitor():
print("\n" + "=" * 100)
print(" T3 — IL MONITOR FORWARD (che alimenta il gate del 23/10) REGISTRA CIO' CHE CREDE?")
print("=" * 100)
if not FWD.exists() or not FWD_STATE.exists():
print(" [stato di paper_xsr assente: non misurabile]")
return
import importlib.util
spec = importlib.util.spec_from_file_location("paper_xsr_ro", ROOT / "scripts" / "live" / "paper_xsr.py")
px = importlib.util.module_from_spec(spec)
spec.loader.exec_module(px) # sola lettura: non si chiama main()
rec = [json.loads(x) for x in FWD.read_text().splitlines() if x.strip()]
st = json.loads(FWD_STATE.read_text())
ts, dt, W, R, rb, syms = px.build_panel()
print(f"\n inception {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} "
f"barre registrate {len(rec)} universo {len(syms)} "
f"(uguale a quello congelato: {syms == st['syms']})")
i0 = int(np.where(ts == st["start_ts"])[0][0])
bk = px._book(px.MODELED_CAPITAL, len(syms))
ric = {}
for i in range(i0 + 1, len(ts)):
ric[int(ts[i])] = px._step(bk, W[i], R[i], float(rb[i]), None)
a = np.array([r["net_modeled"] for r in rec if r["ts"] in ric])
b = np.array([ric[r["ts"]] for r in rec if r["ts"] in ric])
same = int((np.abs(a - b) <= 1e-6).sum())
print(f"\n replay del libro MODELED sui dati di OGGI, dall'inception, contro cio' che il")
print(f" monitor ha REGISTRATO giorno per giorno ({len(a)} barre appaiate):")
print(f" barre identiche entro 1e-6 : {same}/{len(a)}")
print(f" correlazione registrato/ricalcolato: {np.corrcoef(a, b)[0, 1]:+.4f}")
print(f" vol annualizzata REGISTRATA : {a.std()*ANN*100:.2f}%")
print(f" vol annualizzata RICALCOLATA : {b.std()*ANN*100:.2f}%")
print(f" (la strategia e' progettata a vol ~{PUB['vol']:.1f}%: la seconda e' quella giusta)")
frac = (a.std() / b.std()) ** 2
print(f" rapporto di varianza {(a.std()/b.std())**2:.4f} -> in radice di tempo il monitor")
print(f" sta misurando ~{frac*24*60:.0f} minuti di mercato al giorno, non 24 ore.")
print("\n MECCANISMO (leggibile nel codice, non dedotto): `advance()` processa le barre con")
print(" ts > last_ts e poi porta last_ts sull'ULTIMA di esse. Al giro delle 00:30 del giorno D")
print(" l'ultima barra e' quella di D, appena scritta e lunga ~30 minuti: il monitor ne")
print(" registra il rendimento e SPOSTA last_ts oltre. Quando il giro successivo trova la")
print(" barra di D completata, D e' gia' passato. Le 23 ore e mezza restanti di OGNI giorno")
print(" non entrano in nessun rendimento registrato.")
sh_a = float(a.mean() / a.std() * ANN)
sh_b = float(b.mean() / b.std() * ANN)
se = float(np.sqrt(365.0 / len(a))) # SE di uno Sharpe annualizzato su n barre giornaliere
print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}")
print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di")
print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e")
print(" dalla soglia 1.0. Il gate e' il 2026-10-23 e questo script non lo anticipa; qui")
print(" servono solo a mostrare che i due strumenti non misurano la stessa grandezza.")
# ------------------------------------------------------------------ T4
def t4_haircut(P, S):
print("\n" + "=" * 100)
print(" T4 — L'ALTRA META' DEL GATE: l'haircut di eseguibilita' a $5.000 e il min-order")
print("=" * 100)
import importlib.util
spec = importlib.util.spec_from_file_location("paper_xsr_ro2", ROOT / "scripts" / "live" / "paper_xsr.py")
px = importlib.util.module_from_spec(spec)
spec.loader.exec_module(px)
ts, dt, W, R, rb, syms = px.build_panel()
n = len(syms)
def replay(cap0, min_order):
bk = px._book(cap0, n)
out = []
for i in range(len(ts)):
out.append(px._step(bk, W[i], R[i], float(rb[i]), min_order))
r = pd.Series(out, index=pd.to_datetime(ts, unit="ms", utc=True))
tot = float(np.prod(1 + r.values) - 1)
fill = bk["n_fill"] / max(bk["n_fill"] + bk["n_skip"], 1)
return r, tot, fill
print("\n replay dell'INTERA storia in-sample (non della finestra forward: quella e' del gate)")
print(f" {'libro':<28}{'Sharpe':>9}{'ret tot':>10}{'gambe eseguite':>17}{'haircut vs modeled':>21}")
rm, tot_m, _ = replay(5000.0, None)
print(f" {'MODELED (ribil. continuo)':<28}{_sh(rm.values):>9.2f}{tot_m*100:>9.1f}%"
f"{'100%':>17}{'-':>21}")
for mo in (5.0, 10.0, 25.0):
r5, tot5, fill = replay(5000.0, mo)
hc = (tot_m - tot5) / abs(tot_m) if tot_m else float("nan")
flag = "" if hc <= 0.40 else " <-- SOPRA LA GUARDIA 40%"
print(f" {f'REAL $5.000 min-order ${mo:.0f}':<28}{_sh(r5.values):>9.2f}{tot5*100:>9.1f}%"
f"{fill*100:>16.0f}%{hc*100:>20.1f}%{flag}")
# ticket medio per gamba
dW = np.abs(np.diff(W, axis=0, prepend=np.zeros((1, n))))
tick = dW[dW > 0] * 5000.0
print(f"\n ticket per gamba a $5.000: mediano ${np.median(tick):.2f} medio ${tick.mean():.2f}")
print(f" (pubblicato: $14.41 medio) quota di ordini sotto $5: {(tick < 5).mean()*100:.0f}%"
f" sotto $10: {(tick < 10).mean()*100:.0f}%")
print("\n -> il min-order NON e' un dettaglio: e' la variabile che decide la seconda meta' del")
print(" gate, e il numero pubblicato assume $5. Se Hyperliquid e' a $10, la riga da leggere")
print(" il 23/10 e' la seconda, non la prima.")
def main() -> None:
P, S = t1_lenti()
t2_feed(P, S)
t3_monitor()
t4_haircut(P, S)
print("\n" + "=" * 100)
print(" CONTROMISURA PROPOSTA (non implementata: questo script non tocca produzione)")
print("=" * 100)
print("""
Il difetto strutturale e' che `data/raw` e' gitignored e ogni numero di ammissione basato su
hl_* e' irriproducibile per costruzione. La contromisura minima, nella forma che il progetto usa
gia' altrove (guardia `TRONCATO`/`STORIA-CORTA` di `fetch_ib_equities.certify`):
1. IMPRONTA PER FINESTRA CHIUSA. A ogni giro, il certificatore HL scrive in un file versionato
(non in data/raw) `sha256` + conteggio barre + primo/ultimo timestamp per simbolo, calcolati
SOLO sulle barre fino a ieri (finestra chiusa). Un test confronta l'impronta di una finestra
gia' registrata: se cambia, la storia e' stata riscritta e il test rompe. Costa ~50 hash/giorno.
2. LA BARRA IN CORSO VA ETICHETTATA, NON NASCOSTA. `load_hl` (e chiunque legga hl_*) deve poter
chiedere solo le barre CHIUSE. La forma minima e' un flag esplicito con default sicuro; la
forma che il progetto preferisce e' derivare la regola dal codice sorvegliato invece di
ridichiararla in ogni consumatore.
3. I MONITOR FORWARD NON DEVONO CONSUMARE LA BARRA IN CORSO. `advance()` deve fermarsi
all'ultima barra CHIUSA; oggi ne consuma una provvisoria e sposta `last_ts` oltre, perdendo
il 98% di ogni giornata. E' un bug di produzione, non una scelta di modellazione.
RISULTATI PUBBLICATI CHE POGGIANO SU hl_* (tutti da citare con questa riserva):
XS01 (sleeve nel book di ricerca al 15%) — 19 major HL 1d
XSR01 (candidato, gate 23/10) — 50 alt HL 1d
STATARB-MULTI (falsificazione 25/07) — 50 coppie alt/BTC HL
XS-LITE (22/08) — stesse serie
CC01 / breadth alt / dispersione XDISP — stesse serie
Per i BACKTEST l'effetto e' una barra su ~965 (piccolo ma non nullo: qui vale 1.82 -> 1.78).
Per i MONITOR FORWARD l'effetto e' totale, perche' la barra provvisoria e' l'UNICA che leggono.
""")
if __name__ == "__main__":
main()
+910
View File
@@ -0,0 +1,910 @@
#!/usr/bin/env python
"""r0822b_bin_freq.py — BIN-FREQ: il BINARIO di frequenza su SKH01 giudicato come un CANDIDATO.
STORIA DEI FATTI (verificata leggendo il codice, non il riassunto).
1. `r0822_vol_size.py` ha proposto una SIZE PER-TRADE su SKH01 (cella congelata `VTL tv20 w90`
= clip(0.20/rv90(t-1), 0, 3) fissata all'INGRESSO) come CONTROLLO DEL RISCHIO:
+0,070 ISO dShFULL, 23/23 ancore, 8/8 anni.
2. `r0822_volsize_skeptic.py` ha CONFERMATO il numero e FALSIFICATO il meccanismo: il guadagno
e' FREQUENZA (+0,051 sul solo filtro di attivita') e non MAGNITUDINE (+0,003); e la forma
minima e' un BINARIO 0/1 (`BIN L>=med`, 194/406 trade scartati) che vale +0,080 FULL (20/23)
= 115% del LEAD, con controllo di segno superato (l'inverso fa -0,159, 0/23).
3. Lo scettico ha scritto "il candidato giusto non e' la formula ma il binario" — e nessuno
l'ha passato per `study_family_honest`. E' un SOTTOPRODOTTO DI UN ATTACCO, mai dichiarato
come famiglia. Questo script lo giudica come si giudica un candidato.
IPOTESI, REGISTRATA PRIMA DI MISURARE. Mi aspetto che il numero si riproduca (lo scettico e' stato
rigoroso e la sua replica e' bit-exact) e che il candidato NON superi il gate onesto, per due
ragioni dichiarate in anticipo: (a) la soglia e la finestra sono due assi liberi trovati DENTRO
l'esplorazione di un'altra famiglia, quindi la stima di famiglia dovra' essere molto piu' piccola
dell'argmax (precedente: sull'asse w lo scettico stesso stima +0,025 contro +0,080); (b) il
deflated-Sharpe sara' VACUO come lo e' stato sulla famiglia VOL-SIZE — su perturbazioni della
STESSA strategia la varianza fra i trial e' quasi nulla, e il DSR e' ~1 per chiunque, baseline
compreso. Se succede lo dichiaro vacuo e uso un null che ha potenza.
=========================== LA FAMIGLIA, DICHIARATA PRIMA DI GUARDARE =========================
Un binario di frequenza su SKH01 ha esattamente quattro gradi di liberta'. Li dichiaro tutti,
con i loro valori, PRIMA di misurare, e li conto AL RIALZO:
soglia (6) quantile ESPANDENTE causale della vol trailing, q in {0.25, 0.35, 0.50, 0.65,
0.75} + la soglia ASSOLUTA rv <= 20% (== `L>=1` della regola congelata)
finestra (6) w in {45, 60, 90, 120, 180, 270} giorni (sotto 45 lo scettico ha misurato che
la finestra contiene 1-3 barre ATTIVE: campione vuoto, non fragilita')
isteresi (2) h in {0.00, 0.10} (banda +-10% attorno alla soglia, stato persistente causale)
segno (2) +1 tieni la vol BASSA (l'ipotesi) / -1 tieni la vol ALTA (controllo di segno)
timeframe(1) uno solo: la gamba SKH01 vive sulla griglia 230m per costruzione. Dichiarato.
-> 6 x 6 x 2 x 2 = 144 celle
RIAPRIRE UN PARAMETRO RIAPRE LA SUA FAMIGLIA, e questo binario e' nato dentro l'esplorazione di
un'altra famiglia. Percio' al conto delle 144 si SOMMANO i trial gia' spesi sullo stesso oggetto:
28 (r0822_vol_size) + 43 (r0822_volsize_skeptic) = 71.
-> CONTO ONESTO DICHIARATO: 144 + 71 = 215 trial.
La sensibilita' del verdetto al conteggio e' pubblicata (§5), come nel precedente 30/07 dove un
DSR passava a N=8, falliva a N=72 e falliva a N=360.
============================== LENTE, ANCORE, CONVENZIONI =====================================
Lente: path CANONICO di SKH01 (fill al livello, entry a chiusura di bin) — identica nelle due
braccia di ogni confronto, quindi il Delta e' pulito anche se il livello e' pessimistico verso il
path live. Ancore: **23 offset**, la griglia a priori dell'audit 02/07 (ogni 30m su [0,690)):
NON ridotta, perche' il costo misurato e' ~20 ms per cella per ancora. Statistica: MEDIANA DELLE
DIFFERENZE APPAIATE (`A.anchor_luck_delta`), mai differenza delle mediane.
Confronto sempre ISO-PESO: il binario cambia la vol della gamba e quindi il peso EFFETTIVO di
SKH01 nel libro, e muovere quel peso e' gia' stato misurato e RESPINTO il 26/07. Il null del
de-levering e' DEGENERE su una ri-scalatura (`sh(k*base) === sh(base)`), quindi il test e'
iso-peso sullo Sharpe; `k_iso_dd` resta come diagnostica del DD.
Nessuna riscrittura: `extract / equity_daily / leg_daily / sizes_from_L / book / netted_target`
sono IMPORTATE da r0822_vol_size, e il titolo dello scettico e' riprodotto chiamando la SUA
funzione. Replica bit-exact contro `sleeves._skyhook_returns()` prima di ogni delta (§0).
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822b_bin_freq.py
[--every K] 1 ancora ogni K delle 23 (K=8 -> 3 ancore, pilota ~80 s)
[--ndraw N] estrazioni del null di permutazione (default 100)
Con i default gira in ~400 s e ~500 MB di RSS su questa VPS (2 core). Tutti i numeri
riportati qui vengono da questa invocazione, e due esecuzioni indipendenti hanno dato
output numericamente IDENTICI.
"""
from __future__ import annotations
import argparse
import gc
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
sys.path.insert(0, str(_p))
import altlib as A # noqa: E402
import r0702_anchor_skh01 as R # noqa: E402
import r0702_tp01_offset as TO # noqa: E402
import r0822_vol_size as V # noqa: E402 (motore: riusato, non riscritto)
import r0822_volsize_skeptic as SK # noqa: E402 (per riprodurre il titolo)
from r0822_sol_leg import k_iso_dd # noqa: E402
from src.portfolio.portfolio import weights_tilt_null # noqa: E402
from src.strategies.skyhook import SKH01_V1 # noqa: E402
ASSETS = V.ASSETS
OFFSETS_FULL = V.OFFSETS_FULL
sh, vol, maxdd, cagr = V.sh, V.vol, V.maxdd, V.cagr
hold, ins, book = V.hold, V.ins, V.book
CAPITAL, MIN_ORDER = V.CAPITAL, V.MIN_ORDER
W_TP, W_SKH = V.W_TP, V.W_SKH
# ------------------------------------------------------------------ la famiglia (dichiarata sopra)
QS = (0.25, 0.35, 0.50, 0.65, 0.75)
THRS = tuple([("q", q) for q in QS] + [("abs", 0.20)])
WS = (45, 60, 90, 120, 180, 270)
HS = (0.00, 0.10)
SIGNS = (+1, -1)
N_INHERITED = 28 + 43 # r0822_vol_size + r0822_volsize_skeptic, sullo stesso oggetto
CANON = ("q", 0.50, 90, 0.00, +1) # la cella dello scettico, dentro la mia famiglia
L = 112
def cname(thr, w, h, sg) -> str:
t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}"
return f"{t} w{w} h{h:.2f} s{'+' if sg > 0 else '-'}"
# =============================================================== il binario, causale per costruzione
def _hyst(x: np.ndarray, thr: np.ndarray, h: float, keep_below: bool) -> np.ndarray:
"""Stato persistente con banda +-h attorno alla soglia. Causale: al passo t usa solo x_t e
thr_t (entrambi funzione di dati <= t-1) e lo stato al passo t-1. h=0 -> confronto secco."""
n = len(x)
out = np.ones(n, bool)
st = True
for i in range(n):
xi, ti = x[i], thr[i]
if not (np.isfinite(xi) and np.isfinite(ti)):
st = True # warm-up: si tiene tutto (conv. scettico)
elif keep_below:
st = (xi <= ti * (1.0 + h)) if st else (xi <= ti * (1.0 - h))
else:
st = (xi >= ti * (1.0 - h)) if st else (xi >= ti * (1.0 + h))
out[i] = st
return out
class KeepCache:
"""rv e quantili espandenti calcolati UNA volta per (asset, ancora, w). L'espandente e' la
parte cara; il resto e' un confronto."""
def __init__(self) -> None:
self.rv: dict = {}
self.q: dict = {}
def clear(self) -> None:
self.rv.clear()
self.q.clear()
def keep(self, ex: dict, key, thr, w: int, h: float, sg: int) -> pd.Series:
base = ex["base_daily"]
k = (key, w)
if k not in self.rv:
self.rv[k] = V._rv_daily(base, w, active_only=False) # gia' shiftata di 1 giorno
rv = self.rv[k]
if thr[0] == "abs":
t = np.full(len(rv), float(thr[1]))
else:
kq = (key, w, thr[1])
if kq not in self.q:
self.q[kq] = rv.expanding(min_periods=V.VT_WARM).quantile(thr[1])
t = self.q[kq].values
keep = _hyst(rv.values.astype(float), np.asarray(t, float), h, keep_below=(sg > 0))
return pd.Series(keep.astype(float), index=rv.index)
KC = KeepCache()
def rss_mb() -> float:
"""RSS in MB, per non far morire il lavoro degli altri agenti (brief §8: 2 core, ~3 GB)."""
try:
with open("/proc/self/status") as f:
for ln in f:
if ln.startswith("VmRSS:"):
return float(ln.split()[1]) / 1024.0
except Exception:
pass
return float("nan")
def size_bin(thr, w, h, sg):
def f(ex, key):
return V.sizes_from_L(ex, KC.keep(ex, key, thr, w, h, sg))
return f
# =============================================================== motore
def leg_of(EXX: dict, key, fn) -> pd.Series:
return V.leg_daily(EXX, {a: fn(EXX[a], (key, a)) for a in ASSETS})
def iso_pair(TP: pd.Series, leg: pd.Series, leg_base: pd.Series):
"""Libro della variante e libro di CONTROLLO ISO-PESO (baseline ri-scalato alla stessa vol di
gamba => stesso peso effettivo di SKH). Convenzione del bersaglio, riusata tale e quale."""
vb = vol(leg_base)
m = vol(leg) / vb if vb > 0 else 1.0
return book(TP, leg), book(TP, leg_base * m), m
# =============================================================== main
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--every", type=int, default=1)
ap.add_argument("--ndraw", type=int, default=100)
args = ap.parse_args()
offs = OFFSETS_FULL[:: max(1, args.every)]
if len(offs) < 3:
raise SystemExit(f"--every {args.every} lascia {len(offs)} ancore: "
"`A.anchor_luck_delta` ne vuole almeno 3 (usa --every <= 8).")
t0 = time.time()
print("=" * L)
print(" r0822b BIN-FREQ — il BINARIO di frequenza su SKH01, giudicato come un CANDIDATO")
print("=" * L)
print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} della griglia a priori 02/07 (ogni 30m su "
f"[0,690)). NON ridotta.")
print(" lente: path CANONICO, identica nelle due braccia. Confronto sempre ISO-PESO.")
# ---------------------------------------------------------- §1 famiglia, PRIMA di misurare
print("\n" + "-" * L)
print(" 1. GRIGLIA DICHIARATA — scritta prima di guardare i risultati, contata AL RIALZO")
print("-" * L)
cells = [(t, w, h, s) for t in THRS for w in WS for h in HS for s in SIGNS]
print(f" soglia {len(THRS):>3} {['q%.2f' % q for q in QS] + ['rv<=20% (== L>=1)']}")
print(f" finestra {len(WS):>3} {list(WS)} giorni")
print(f" isteresi {len(HS):>3} {list(HS)} (banda +-h attorno alla soglia)")
print(f" segno {len(SIGNS):>3} +1 tieni vol BASSA (ipotesi) / -1 tieni vol ALTA (controllo)")
print(f" timeframe 1 la gamba SKH01 vive su 230m per costruzione")
print(f" -> celle di QUESTA famiglia: {len(cells)}")
print(f" -> trial gia' spesi sullo STESSO oggetto: {N_INHERITED} "
f"(28 r0822_vol_size + 43 r0822_volsize_skeptic)")
print(f" -> CONTO ONESTO DICHIARATO: {len(cells)} + {N_INHERITED} = "
f"{len(cells) + N_INHERITED} trial. La sensibilita' al conteggio e' in §5.")
assert len(cells) == 144
# ---------------------------------------------------------- estrazione
print("\n estrazione tabelle trade (V2-DD) ...", flush=True)
EX = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs}
ntr = {a: int(np.mean([len(EX[o][a]["i_ent"]) for o in offs])) for a in ASSETS}
R.get5m.cache_clear() # ~90 MB di 5m non servono durante la §4 (la VPS ha 2 core
gc.collect() # e ~3 GB liberi condivisi); si ricaricano in §9b in ~3 s
print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " +
", ".join(f"{a} {ntr[a]}" for a in ASSETS) + f" (RSS {rss_mb():.0f} MB)")
TP = A.tp01_baseline_daily()
can = offs[0]
# ---------------------------------------------------------- §0 SANITY
print("\n" + "-" * L)
print(" 0. SANITY — bit-exact prima di qualunque delta")
print("-" * L)
from src.portfolio.sleeves import _skyhook_returns
base_leg = {o: V.leg_daily(EX[o], {a: V.size_flat(EX[o][a]) for a in ASSETS}) for o in offs}
off = _skyhook_returns()
d0 = float(np.max(np.abs(base_leg[can].values - off.values))) if len(base_leg[can]) == len(off) \
else float("nan")
print(f" gamba 50/50 off{can} vs sleeves._skyhook_returns(): max|dif| = {d0:.2e} "
f"({len(base_leg[can])} giorni)")
assert d0 < 1e-15, "replica NON bit-exact"
for a in ASSETS:
ref = R.run_asset(a, can)[0]
R._CACHE.clear()
dd = float(np.max(np.abs(V.equity_daily(EX[can][a], V.size_flat(EX[can][a])).values
- ref.values)))
print(f" {a} vs r0702_anchor_skh01.run_asset: max|dif| = {dd:.2e}")
assert dd < 1e-15
# ---------------------------------------------------------- §2 riproduzione del titolo
print("\n" + "-" * L)
print(" 2. RIPRODUZIONE DEL TITOLO DELLO SCETTICO — chiamando la SUA funzione, non la mia")
print(" (pubblicato: BIN L>=med +0,080 FULL 20/23 ; INVERSO -0,159 0/23)")
print("-" * L)
REP = {}
for tag, mode in (("BIN L>=med (scettico)", "gemed"), ("BIN L<1 INVERSO (scettico)", "lt1"),
("BIN L>=1 (scettico)", "ge1")):
fn = SK.size_bin(SK.FROZEN, mode)
bk, ct = {}, {}
for o in offs:
lg = V.leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS})
bk[o], ct[o], _ = iso_pair(TP, lg, base_leg[o])
REP[tag] = (bk, ct)
dF = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh)
dH = A.anchor_luck_delta(lambda o: hold(bk[o]), lambda o: hold(ct[o]), offs, metric=sh)
print(f" {tag:<28} ISO dShFULL {dF['median_paired']:+.3f} "
f"({dF['n_positive']}/{len(offs)}) banda [{dF['lo']:+.3f},{dF['hi']:+.3f}] "
f"ISO dShHOLD {dH['median_paired']:+.3f} ({dH['n_positive']}/{len(offs)})")
med_F = float(np.median([sh(book(TP, base_leg[o])) for o in offs]))
med_H = float(np.median([sh(hold(book(TP, base_leg[o]))) for o in offs]))
med_D = float(np.median([maxdd(book(TP, base_leg[o])) for o in offs]))
ban_F = [sh(book(TP, base_leg[o])) for o in offs]
print(f" libro BASE, mediana della banda: ShFULL {med_F:.3f} / ShHOLD {med_H:.3f} / "
f"maxDD {med_D*100:.1f}% [pubblicato 1,626 / 1,037 / 10,4%]")
print(f" banda d'ancora del LIVELLO del libro: ShFULL [{min(ban_F):.3f},{max(ban_F):.3f}] "
f"= ampiezza {max(ban_F)-min(ban_F):.3f} (il termine di paragone di §12)")
# ---------------------------------------------------------- §3 causalita' per troncamento
print("\n" + "-" * L)
print(" 3. CAUSALITA' — la decisione tenere/scartare ricalcolata TRONCANDO i dati al giorno")
print(" d'ingresso. `A.causality_ok` NON E' GIRATO (vuole un target_fn(df) e qui il")
print(" segnale nasce da 5m->230m: ri-eseguirlo su prefissi costa quanto tutto lo")
print(" script). Sostituito da un troncamento diretto, piu' stringente sul punto esatto.")
print("-" * L)
thr_c, w_c, h_c, s_c = ("q", CANON[1]), CANON[2], CANON[3], CANON[4]
bad = tested = 0
for a in ASSETS:
ex = EX[can][a]
kfull = KC.keep(ex, (can, a), thr_c, w_c, h_c, s_c)
rv_full = V._rv_daily(ex["base_daily"], w_c, False)
days = list(dict.fromkeys(ex["day_ent"]))
for d in days[::7]: # 1 ingresso su 7, su tutta la storia
if d not in kfull.index:
continue
b_tr = ex["base_daily"][ex["base_daily"].index <= d]
if len(b_tr) < V.VT_WARM + w_c:
continue
rv_t = V._rv_daily(b_tr, w_c, False)
q_t = rv_t.expanding(min_periods=V.VT_WARM).quantile(thr_c[1])
k_t = _hyst(rv_t.values.astype(float), q_t.values.astype(float), h_c, s_c > 0)[-1]
tested += 1
if bool(k_t) != bool(kfull.loc[d] > 0.5):
bad += 1
assert abs(float(rv_t.iloc[-1]) - float(rv_full.loc[d])) < 1e-12 or \
not np.isfinite(rv_full.loc[d])
print(f" decisioni ricalcolate su prefisso: {tested} DIVERGENTI: {bad}")
print(" NB l'isteresi ha memoria: troncando si perde lo stato, quindi il test e' esatto")
print(" per h=0 (la cella canonica) e conservativo per h>0.")
assert bad == 0, "il binario NON e' causale"
# ---------------------------------------------------------- §4 tabella di famiglia
print("\n" + "-" * L)
print(" 4. LA FAMIGLIA INTERA — ISO dShFULL, mediana delle differenze appaiate su "
f"{len(offs)} ancore")
print("-" * L)
# RAM (brief §8: 7 GB totali, ~3 liberi, e siamo in tanti). Tre accorgimenti, tutti
# necessari — la prima stesura di questo script e' stata UCCISA a meta' della §4:
# (a) DUE PASSATE: la 1a calcola le metriche per-ancora di tutte le 144 celle SENZA
# conservare le serie (144 x 23 x 3 serie da 2717 punti = ~500 MB); la 2a ricalcola e
# conserva le serie delle sole 3-4 celle che servono dopo (~2 s);
# (b) ciclo ANCORA-ESTERNO con la cache di rv/quantili SVUOTATA a ogni ancora: viva una sola
# ancora alla volta (72 serie invece di 1.656);
# (c) RSS stampata, cosi' se qualcuno la rilancia sa cosa aspettarsi.
BK, CT, LEG, MS = {}, {}, {}, {}
ACC: dict = {}
def _one(o, thr, w, h, sg, store: bool):
"""UNA cella a UNA ancora. Le size si calcolano una volta sola e si riusano (la prima
stesura le ricalcolava per il conteggio dei tenuti); la scomposizione per anno e il maxDD
si fanno SOLO nella seconda passata, perche' servono a 3 celle e costavano il 60% del
tempo su tutte e 144."""
nm = cname(thr, w, h, sg)
fn = size_bin(thr, w, h, sg)
szs = {a: fn(EX[o][a], (o, a)) for a in ASSETS}
lg = V.leg_daily(EX[o], szs)
b, c, m = iso_pair(TP, lg, base_leg[o])
d = ACC.setdefault(nm, dict(thr=thr, w=w, h=h, sg=sg, dF=[], dH=[], dI=[], dC=[],
lF=[], lI=[], lH=[], ms=[], kp=[], ddv=[], ddc=[], yr={}))
d["dF"].append(sh(b) - sh(c)); d["dH"].append(sh(hold(b)) - sh(hold(c)))
d["dI"].append(sh(ins(b)) - sh(ins(c)))
d["lF"].append(sh(lg)); d["lI"].append(sh(ins(lg))); d["lH"].append(sh(hold(lg)))
d["ms"].append(m)
d["kp"].append(float(np.mean(np.concatenate(list(szs.values())))))
if store:
d["dC"].append(cagr(b) - cagr(c))
d["ddv"].append(maxdd(b)); d["ddc"].append(maxdd(c))
for y in {int(t) for t in b.index.year}:
by, cy = b[b.index.year == y], c[c.index.year == y]
if len(by) > 20:
d["yr"].setdefault(y, []).append(sh(by) - sh(cy))
BK.setdefault(nm, {})[o] = b
CT.setdefault(nm, {})[o] = c
LEG.setdefault(nm, {})[o] = lg
MS.setdefault(nm, {})[o] = m
for o in offs:
KC.clear()
for (thr, w, h, sg) in cells:
_one(o, thr, w, h, sg, store=False)
KC.clear()
def _med(v, dflt=float("nan")):
return float(np.median(v)) if len(v) else dflt
def _fin(d: dict) -> dict:
a_ = np.asarray(d["dF"], float)
return dict(nm=cname(d["thr"], d["w"], d["h"], d["sg"]), thr=d["thr"], w=d["w"],
h=d["h"], sg=d["sg"], dF=float(np.median(a_)), nF=int((a_ > 0).sum()),
lo=float(a_.min()), hi=float(a_.max()), arrF=a_,
dH=float(np.median(d["dH"])), nH=int((np.asarray(d["dH"]) > 0).sum()),
isoI=float(np.median(d["dI"])), dC=_med(d["dC"]),
legF=float(np.median(d["lF"])), legI=float(np.median(d["lI"])),
legH=float(np.median(d["lH"])), kept=float(np.median(d["kp"])),
weff=float(np.median([W_SKH * x for x in d["ms"]])),
ddv=_med(d["ddv"]), ddc=_med(d["ddc"]),
yr={y: float(np.median(v)) for y, v in d["yr"].items()})
RES = {nm: _fin(d) for nm, d in ACC.items()}
print(f" ({len(cells)} celle x {len(offs)} ancore valutate in {time.time()-t0:.0f}s "
f"dall'avvio, prima passata senza conservare le serie; RSS {rss_mb():.0f} MB)")
def matrix(sg: int, h: float) -> None:
print(f"\n ISO dShFULL — segno {'+1 (tieni vol BASSA)' if sg>0 else '-1 (CONTROLLO)'}"
f", isteresi h={h:.2f}")
print(" soglia " + "".join(f"{('w%d' % w):>11}" for w in WS))
for thr in THRS:
t = "rv<=20%" if thr[0] == "abs" else f"q{thr[1]:.2f}"
row = ""
for w in WS:
r = RES[cname(thr, w, h, sg)]
row += f"{r['dF']:>+8.3f}{r['nF']:>3}"
print(f" {t:<10}" + row)
for sg in SIGNS:
for h in HS:
matrix(sg, h)
print("\n (ogni cella: mediana appaiata e n. di ancore positive su "
f"{len(offs)})")
POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS]
NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS]
ALLC = [RES[cname(t, w, h, s)] for t in THRS for w in WS for h in HS for s in SIGNS]
nmcan = cname(("q", CANON[1]), CANON[2], CANON[3], CANON[4])
argmax = max(ALLC, key=lambda r: r["dF"])
print(f"\n ARGMAX della famiglia: {cname(argmax['thr'], argmax['w'], argmax['h'], argmax['sg']):<26}"
f" dShFULL {argmax['dF']:+.3f} ({argmax['nF']}/{len(offs)})")
print(f" cella dello scettico: {nmcan:<26} dShFULL {RES[nmcan]['dF']:+.3f} "
f"({RES[nmcan]['nF']}/{len(offs)}) rango "
f"{1 + sum(r['dF'] > RES[nmcan]['dF'] for r in ALLC)}/{len(ALLC)}")
print(f" STIMA ONESTA se la cella si sceglie a caso dentro la famiglia (mediana):")
print(f" tutta la famiglia (144) {float(np.median([r['dF'] for r in ALLC])):+.3f} "
f"positive {sum(r['dF']>0 for r in ALLC)}/{len(ALLC)}")
print(f" solo segno +1 (72) {float(np.median([r['dF'] for r in POS])):+.3f} "
f"positive {sum(r['dF']>0 for r in POS)}/{len(POS)}")
print(f" solo segno -1 (72) {float(np.median([r['dF'] for r in NEG])):+.3f} "
f"positive {sum(r['dF']>0 for r in NEG)}/{len(NEG)} <- controllo di segno")
# seconda passata: conserva le serie delle sole celle che servono dopo
sel_iso0 = max(ALLC, key=lambda r: r["isoI"])
ACC2 = ACC
for r in dict((x["nm"], x) for x in (RES[nmcan], argmax, sel_iso0,
max(ALLC, key=lambda z: z["legI"]))).values():
ACC = {}
for o in offs:
_one(o, r["thr"], r["w"], r["h"], r["sg"], store=True)
KC.clear()
RES[r["nm"]] = _fin(ACC[r["nm"]]) # ora con dC / maxDD / per-anno
ACC = ACC2
POS = [RES[cname(t, w, h, +1)] for t in THRS for w in WS for h in HS]
NEG = [RES[cname(t, w, h, -1)] for t in THRS for w in WS for h in HS]
ALLC = POS + NEG
argmax = RES[argmax["nm"]]
dchk = A.anchor_luck_delta(lambda o: BK[nmcan][o], lambda o: CT[nmcan][o], offs, metric=sh)
assert abs(dchk["median_paired"] - RES[nmcan]["dF"]) < 1e-12 and \
dchk["n_positive"] == RES[nmcan]["nF"], "la statistica appaiata di §4 non coincide con A.anchor_luck_delta"
print(f" controllo: la statistica appaiata calcolata in §4 coincide con "
f"`A.anchor_luck_delta` ({dchk['median_paired']:+.6f})")
print(f" quota di trade TENUTI, mia cella q0.50 w90: {RES[nmcan]['kept']*100:.1f}% "
f"(mediana fra le ancore)")
fn_can = size_bin(("q", CANON[1]), CANON[2], CANON[3], CANON[4])
fn_sk = SK.size_bin(SK.FROZEN, "gemed")
ov, k_me, k_sk = [], [], []
for o in offs:
for a in ASSETS:
x = fn_can(EX[o][a], (o, a)) > 0.5
y = fn_sk(EX[o][a]) > 0.5
ov.append(float((x == y).mean())); k_me.append(float(x.mean())); k_sk.append(float(y.mean()))
dleg = float(np.max(np.abs(BK[nmcan][can].values - REP["BIN L>=med (scettico)"][0][can].values)))
print(f" ⚠ la MIA parametrizzazione NON e' bit-identica a quella dello scettico: lui prende")
print(f" la mediana espandente di L = clip(0.20/rv,0,3) DOPO un `.fillna(1.0)` che inietta")
print(f" dei 1.0 nel warm-up; io prendo il quantile espandente di rv, che salta i NaN. Il")
print(f" trasformo e' monotono, quindi le due decisioni coincidono nel "
f"{float(np.mean(ov))*100:.1f}% dei trade")
print(f" (tenuti {float(np.mean(k_me))*100:.1f}% io / {float(np.mean(k_sk))*100:.1f}% lui); "
f"max|dif| fra i due libri = {dleg:.2e}.")
# ---------------------------------------------------------- §5 i tre componenti del gate
print("\n" + "-" * L)
print(" 5. `study_family_honest` — NON chiamabile direttamente (e' cablato sui candidati")
print(" DIREZIONALI: factory -> target_fn via candidate_daily, e qui il candidato e' un")
print(" overlay su uno sleeve). Come il 30/07 sul tenore VRP01, uso i suoi TRE")
print(" COMPONENTI REALI: selezione in-sample-only -> deflated_sharpe -> marginal_vs_tp01.")
print("-" * L)
print(" (a) SELEZIONE IN-SAMPLE-ONLY — due criteri, entrambi dichiarati, entrambi sulla")
print(" MEDIANA fra le ancore (l'ancora non e' un hold-out, e' una dimensione di luck)")
sel_leg = max(ALLC, key=lambda r: r["legI"]) # come `select_cell_insample`
sel_iso = max(ALLC, key=lambda r: r["isoI"]) # cio' su cui si decide davvero
hop_leg = max(ALLC, key=lambda r: r["legH"])
hop_iso = max(ALLC, key=lambda r: r["dH"])
for tag, r in (("Sharpe IS standalone della gamba (come select_cell_insample)", sel_leg),
("ISO dShFULL IN-SAMPLE del libro (cio' che decide)", sel_iso)):
n = cname(r["thr"], r["w"], r["h"], r["sg"])
print(f" scelta al buio su {tag}")
print(f" -> {n:<26} ISO dShFULL {r['dF']:+.3f} ({r['nF']}/{len(offs)}) "
f"ISO dShHOLD {r['dH']:+.3f} ({r['nH']}/{len(offs)}) banda "
f"[{r['lo']:+.3f},{r['hi']:+.3f}]")
for tag, r in (("Sharpe HOLD-OUT standalone", hop_leg), ("ISO dShHOLD del libro", hop_iso)):
print(f" controllo (scegliendo SULL'HOLD-OUT, cio' che NON si fa) su {tag}:")
print(f" -> {cname(r['thr'], r['w'], r['h'], r['sg']):<26} dShFULL {r['dF']:+.3f}"
f" dShHOLD {r['dH']:+.3f}")
CHOSEN = sel_iso
nmc = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"])
print(f" CELLA SCELTA AL BUIO (criterio primario = ISO dShFULL in-sample): {nmc}")
print("\n (b) DEFLATED SHARPE — sullo Sharpe FULL standalone della gamba (la grandezza che")
print(" `select_cell_insample` usa), mediana fra le ancore; `daily` della barra canonica")
print(" serve solo per T/asimmetria/curtosi. Sensibilita' al CONTEGGIO ottenuta")
print(" REPLICANDO la distribuzione dei trial osservati (tiling), non riempiendola con")
print(" la mediana: riempire con la mediana ABBASSA la varianza dei trial e fa SALIRE il")
print(" DSR all'aumentare di N, che e' il verso sbagliato. (Errore mio, catturato nel")
print(" pilota e corretto: il primo output dava DSR 0,966 -> 0,986 -> 0,998 da N=144 a")
print(" N=500, cioe' 'piu' trial = piu' significativo'.)")
all_sr = [r["legF"] for r in ALLC]
pos_sr = [r["legF"] for r in POS]
base_sr = float(np.median([sh(base_leg[o]) for o in offs]))
dly = LEG[nmc][can]
def tile(v, N):
return (list(v) * (N // len(v) + 1))[:N]
for lab, sr, d in (("cella scelta al buio", CHOSEN["legF"], dly),
("BASELINE (size fissa)", base_sr, base_leg[can])):
for N, tagN in ((len(ALLC), f"{len(ALLC)} di QUESTA famiglia"),
(len(ALLC) + N_INHERITED,
f"{len(ALLC)+N_INHERITED} = conto onesto (+{N_INHERITED} ereditati)"),
(500, "500 (ipotetico, al rialzo)")):
dsr, sr0 = A.deflated_sharpe(sr, tile(all_sr, N), d)
print(f" {lab:<22} N={tagN:<44} DSR {dsr:.3f} max atteso sotto il null {sr0:+.3f}")
lo_n, hi_n = len(ALLC), 20000
if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, hi_n), dly)[0] >= 0.95:
cross = f">{hi_n}"
elif A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, lo_n), dly)[0] < 0.95:
cross = f"<{lo_n}"
else:
for _ in range(40):
mid = (lo_n + hi_n) // 2
if A.deflated_sharpe(CHOSEN["legF"], tile(all_sr, mid), dly)[0] >= 0.95:
lo_n = mid
else:
hi_n = mid
cross = f"~{lo_n}"
print(f" -> IL VERDETTO SI RIBALTA A N {cross} trial: sotto quel conteggio il DSR passa,")
print(f" sopra fallisce. Il conto onesto dichiarato ({len(ALLC)+N_INHERITED}) sta dal lato")
print(f" del PASS. Precedente 30/07: un DSR passava a N=8, falliva a N=72 e a N=360; qui")
print(f" la stessa sensibilita' e' pubblicata invece di essere taciuta.")
dsp, sr0p = A.deflated_sharpe(CHOSEN["legF"], tile(pos_sr, len(POS) + N_INHERITED), dly)
print(f" sensibilita' alla COMPOSIZIONE della famiglia: escludendo il segno -1 (che e' un")
print(f" controllo e non un candidato) i trial diventano {len(POS)}+{N_INHERITED} e la loro")
print(f" dispersione cala -> DSR {dsp:.3f} (max atteso {sr0p:+.3f}). Includere il segno -1")
print(f" e' la scelta CONSERVATIVA e resta quella dichiarata.")
print(f" dispersione dei trial: sd(Sharpe FULL) = {float(np.std(all_sr, ddof=1)):.3f} sulle")
print(f" {len(ALLC)} celle, {float(np.std(pos_sr, ddof=1)):.3f} sulle {len(POS)} di segno +1.")
print(f" -> il DSR qui NON e' vacuo (il baseline lo FALLISCE dove il candidato lo passa):")
print(f" la famiglia contiene celle davvero diverse fra loro, al contrario della famiglia")
print(f" VOL-SIZE dove erano tutte perturbazioni della stessa size continua.")
print(f" ❌ QUESTO REFUTA LA MIA IPOTESI (b), registrata nel docstring PRIMA di misurare")
print(f" ('il DSR sara' vacuo come su VOL-SIZE'). La ragione per cui mi sbagliavo e'")
print(f" strutturale e vale come regola: un DSR e' vacuo quando i trial si somigliano")
print(f" (var_tr -> 0 => sr0 -> 0 => DSR ~ 1 per chiunque). Una famiglia BINARIA contiene")
print(f" celle che scartano insiemi di trade MOLTO diversi — compreso il segno inverso —")
print(f" quindi la dispersione dei trial e' reale e il test riacquista potenza. Non e' il")
print(f" binario a essere piu' solido della formula: e' la FAMIGLIA a essere piu' larga.")
print("\n (c) MARGINAL vs TP01 — sulla gamba modificata e sulla gamba base")
for lab, s in (("BASE (size fissa)", base_leg[can]), (nmc, LEG[nmc][can]),
("cella dello scettico", LEG[nmcan][can])):
m = A.marginal_vs_tp01(s)
b25 = m.get("blends", {}).get("w25", {})
print(f" {lab:<26} {str(m.get('marginal_verdict')):<10} corr {m.get('corr_full')} "
f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')} "
f"robust_oos={m.get('robust_oos')} insample_edge={m.get('has_insample_edge')} "
f"hedge={m.get('is_hedge')}")
imp = A.implausible_sharpe(
LEG[nmc][can],
n_trades=int(sum(len(EX[can][a]["net"]) for a in ASSETS)),
n_losing_trades=int(sum((EX[can][a]["net"] < 0).sum() for a in ASSETS)))
print(f" implausible_sharpe (gamba scelta): implausible={imp['implausible']} "
f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive "
f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}")
# ---------------------------------------------------------- §6 null con potenza
print("\n" + "-" * L)
print(" 6. IL NULL CHE HA POTENZA — permutazione della SEQUENZA tenuti/scartati.")
print(" Stessa quota di trade tenuti, riassegnata a caso: separa 'conta QUALE trade si")
print(" scarta' (informazione) da 'conta scartarne meta'' (aritmetica). Una mediana del")
print(" null lontana da 0 direbbe che il null non e' un null (lezione 22/08 sul null")
print(f" invertito). {args.ndraw} estrazioni su TUTTE le {len(offs)} ancore, non su 3.")
print("-" * L)
rng = np.random.default_rng(20260822)
for lab, r in (("cella scelta al buio", CHOSEN), ("cella dello scettico", RES[nmcan]),
("argmax di famiglia", argmax)):
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
real = float(np.median([sh(BK[nm][o]) - sh(CT[nm][o]) for o in offs]))
fn = size_bin(r["thr"], r["w"], r["h"], r["sg"])
sz0 = {o: {a: fn(EX[o][a], (o, a)) for a in ASSETS} for o in offs}
nul = []
for _ in range(args.ndraw):
v = []
for o in offs:
szp = {a: rng.permutation(sz0[o][a]) for a in ASSETS}
lg = V.leg_daily(EX[o], szp)
b, c, _ = iso_pair(TP, lg, base_leg[o])
v.append(sh(b) - sh(c))
nul.append(float(np.median(v)))
nul = np.asarray(nul)
pc = float((nul < real).mean())
print(f" {lab:<24} {nm:<24} reale {real:+.3f} null mediana {np.median(nul):+.3f} "
f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} -> p ~ {1-pc:.3f}")
print(f" (seme 20260822; {args.ndraw} estrazioni x {len(offs)} ancore)")
print("\n ⚠ LA MEDIANA DEL NULL NON E' ~0, E NON E' UN DIFETTO DEL NULL: E' UN FATTO.")
print(" Scartare a caso meta' dei trade DANNEGGIA, perche' lo Sharpe ANNUALIZZATO cresce")
print(" con la radice del numero di scommesse per anno: dimezzare i trade sullo stesso")
print(" arco di calendario costa un fattore ~sqrt(2) di Sharpe di gamba. Misurato:")
nmv = cname(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"])
fnv = size_bin(CHOSEN["thr"], CHOSEN["w"], CHOSEN["h"], CHOSEN["sg"])
rng2 = np.random.default_rng(20260823)
shb = float(np.median([sh(base_leg[o]) for o in offs]))
shv = float(np.median([sh(LEG[nmv][o]) for o in offs]))
kp = float(np.median([np.mean(np.concatenate([fnv(EX[o][a], (o, a)) for a in ASSETS]))
for o in offs]))
shn = []
for _ in range(min(40, args.ndraw)):
sz0 = {o: {a: fnv(EX[o][a], (o, a)) for a in ASSETS} for o in offs}
shn.append(float(np.median([sh(V.leg_daily(EX[o], {a: rng2.permutation(sz0[o][a])
for a in ASSETS})) for o in offs])))
print(f" Sharpe della GAMBA: baseline (tutti i trade) {shb:.3f} -> permutata "
f"{float(np.median(shn)):.3f} -> filtrata {shv:.3f}")
print(f" quota tenuta {kp*100:.0f}% -> attesa dalla sola perdita di scommesse "
f"{shb*np.sqrt(kp):.3f}; osservata sotto permutazione {float(np.median(shn)):.3f}")
print(" Conseguenza per la lettura: il null NON e' centrato su zero, quindi il p-value")
print(" risponde alla domanda 'la SELEZIONE batte una selezione casuale della stessa")
print(" taglia?' e non alla domanda 'il filtro batte il non filtrare?'. La seconda e' il")
print(" confronto con la BASE (§4), che e' quello su cui si decide. Il filtro deve")
print(" superare una penale meccanica prima di guadagnare qualcosa: e' un test SEVERO.")
# ---------------------------------------------------------- §7 iso-peso / de-levering
print("\n" + "-" * L)
print(" 7. ISO-PESO e NULL DEL DE-LEVERING")
print("-" * L)
print(f" {'cella':<26}{'wSKH eff':>10}{'dShFULL RAW':>13}{'ISO':>9}{'maxDD var':>11}"
f"{'maxDD ctrl':>11}{'k_iso_dd':>10}")
for lab, r in (("BASE", None), ("cella dello scettico", RES[nmcan]),
("cella scelta al buio", CHOSEN)):
if r is None:
print(f" {'BASE (size fissa)':<26}{W_SKH:>10.3f}{0.0:>+13.3f}{0.0:>+9.3f}"
f"{maxdd(book(TP, base_leg[can]))*100:>10.1f}%{'-':>11}{'-':>10}")
continue
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
raw = A.anchor_luck_delta(lambda o, n=nm: BK[n][o],
lambda o: book(TP, base_leg[o]), offs, metric=sh)
dv = float(np.median([maxdd(BK[nm][o]) for o in offs]))
dc = float(np.median([maxdd(CT[nm][o]) for o in offs]))
ks = (f"{k_iso_dd(CT[nm][can], BK[nm][can]):.3f}" if dv < dc
else "n/a") # n/a = non riduce il DD: nulla da negare
print(f" {nm:<26}{r['weff']:>10.3f}{raw['median_paired']:>+13.3f}{r['dF']:>+9.3f}"
f"{dv*100:>10.1f}%{dc*100:>10.1f}%{ks:>10}")
print(" Il null del de-levering e' DEGENERE su una ri-scalatura (sh(k*base) === sh(base)):")
print(" qui il test e' l'iso-peso sullo Sharpe. `k_iso_dd` e' riportato come diagnostica del")
print(" DD ed e' calcolato CONTRO il controllo iso-peso (non contro il baseline nudo).")
print(" NB il binario ABBASSA il peso effettivo di SKH01, non lo alza: la preoccupazione del")
print(" 26/07 ('alzare SKH01') non morde, ma l'iso-peso resta obbligatorio nell'altro verso.")
print(f"\n LIVELLI del libro (mediana fra le {len(offs)} ancore), netti fee:")
print(f" {'configurazione':<30}{'ShFULL':>9}{'ShHOLD':>9}{'ShIS':>8}{'maxDD':>9}{'CAGR':>9}"
f"{'vol':>8}")
lev = [("BASE (libro live 75/25)", {o: book(TP, base_leg[o]) for o in offs})]
for lab, r in (("BIN, peso eff. lasciato cadere", RES[nmcan]),
("BIN, controllo ISO-PESO", None)):
if r is None:
lev.append((lab, {o: CT[nmcan][o] for o in offs}))
else:
lev.append((lab, {o: BK[nmcan][o] for o in offs}))
for lab, dd_ in lev:
f_ = float(np.median([sh(dd_[o]) for o in offs]))
h_ = float(np.median([sh(hold(dd_[o])) for o in offs]))
i_ = float(np.median([sh(ins(dd_[o])) for o in offs]))
d_ = float(np.median([maxdd(dd_[o]) for o in offs]))
c_ = float(np.median([cagr(dd_[o]) for o in offs]))
v_ = float(np.median([vol(dd_[o]) for o in offs]))
print(f" {lab:<30}{f_:>9.3f}{h_:>9.3f}{i_:>8.3f}{d_*100:>8.1f}%{c_*100:>8.1f}%"
f"{v_*100:>7.1f}%")
print(" ⚠ la DIFFERENZA di queste mediane NON e' la mediana delle differenze appaiate (§4):")
print(" qui le mediane cadono su ancore diverse. La tabella serve a dare i LIVELLI netti fee")
print(" richiesti dal contratto; il numero che decide resta quello appaiato.")
# ---------------------------------------------------------- §8 weights_tilt_null
print("\n" + "-" * L)
print(" 8. `weights_tilt_null` — una modulazione binaria della size E' un cambio di pesi nel")
print(" tempo. Il gate confronta vettori STATICI: cattura la sola componente di SCALA")
print(" (limite dichiarato, come nel bersaglio). k_seen = tutti i trial dichiarati.")
print("-" * L)
cols = {"TP01": TP, "SKH01": base_leg[can]}
for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN),
("argmax di famiglia", argmax)):
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
m = MS[nm][can]
wp = {"TP01": W_TP, "SKH01": W_SKH * m}
g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, caps={"SKH01": 0.5},
floor=0.05, n=300, k_seen=len(ALLC) + N_INHERITED)
print(f" {lab:<24} {nm:<24} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} "
f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} "
f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}")
print(" ⚠ LIMITE, e va detto perche' e' aggirabile: il binario ABBASSA il peso effettivo di")
print(" SKH01, e il gate misura proprio quella caduta. Nella forma di deploy naturale (si")
print(" ri-scala la gamba per riportare il peso effettivo a 0,25) il vettore proposto e'")
print(" IDENTICO a quello corrente -> d_IS = d_HOLD = 0 e il gate diventa INERTE, non")
print(" superato. Quindi `weights_tilt_null` NON puo' essere l'evidenza decisiva qui: e'")
print(" negativo nella forma nuda e vacuo nella forma compensata. Cio' che decide resta la")
print(" banda APPAIATA a iso-peso (§4) e la stima di famiglia.")
# ---------------------------------------------------------- §9 eseguibilita'
print("\n" + "-" * L)
print(f" 9. ESEGUIBILITA' a ${CAPITAL:.0f} — il vantaggio DICHIARATO dal filone (dimezza gli")
print(" ordini). Target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h = cio' che il cron manda.")
print("-" * L)
print(f" {'configurazione':<26}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}"
f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}")
EXEC = {}
for lab, r in (("BASE (size fissa)", None), ("cella dello scettico", RES[nmcan]),
("cella scelta al buio", CHOSEN)):
nm = None if r is None else "SEL"
tm = tr = tu = 0.0
no = ns = 0
for a in ASSETS:
df1h = TO.get1h(a)
SZa = {} if r is None else {
"SEL": (lambda ex, rr=r, aa=a: size_bin(rr["thr"], rr["w"], rr["h"],
rr["sg"])(ex, (can, aa)))}
tgt = V.netted_target(a, EX[can][a], nm, SZa, {}, {}, {}, {}, TP, can, df1h)
ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER)
tm += 0.5 * ev["modeled"]["sharpe"]
tr += 0.5 * ev["realistic"]["sharpe"]
tu += ev["executed_turnover_per_year"]
no += ev["n_executed_trades"]
dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0))
ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum())
EXEC[lab] = (tm, tr, no, ns, tu)
print(f" {lab:<26}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}")
print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.")
ntr_base = int(sum(len(EX[can][a]['i_ent']) for a in ASSETS))
ntr_sel = int(sum((size_bin(CHOSEN['thr'], CHOSEN['w'], CHOSEN['h'], CHOSEN['sg'])
(EX[can][a], (can, a)) > 0).sum() for a in ASSETS))
print(f" trade SKH eseguiti (off{can}): BASE {ntr_base} -> cella scelta {ntr_sel} "
f"({ntr_sel/ntr_base*100:.0f}%)")
print(" ⚠ IL RISPARMIO DI FEE E' GIA' DENTRO i numeri sopra: `extract` netta ogni trade di")
print(f" FEE_RT={V.FEE_RT:.4f} (10 bps RT) all'ingresso, quindi un trade scartato non paga.")
print(" Convertirlo di nuovo con la curva del 26/07 (-0,017 Sh/bps di libro, SKH 4x TP01)")
print(" sarebbe un DOPPIO CONTEGGIO. Cio' che NON e' dentro e' (a) il min-order a $635,")
print(" misurato qui sopra come haircut, e (b) il fatto che la fee reale e' 3,5 bps/lato =")
print(" 7 bps RT contro i 10 modellati -> i backtest sono CONSERVATIVI, in entrambe le braccia.")
# ---------------------------------------------------------- §9b fee vs informazione
print("\n" + "-" * L)
print(" 9b. QUANTO DELL'EFFETTO E' INFORMAZIONE E QUANTO E' SOLO FEE RISPARMIATA.")
print(" Scartare meta' dei trade risparmia meta' delle fee: parte del +0,080 e' quella,")
print(" ed e' un guadagno vero ma NON e' un'ipotesi sulla vol. Lo stesso confronto,")
print(" identico in tutto, ricalcolato a fee diverse (le size si ricalcolano da zero,")
print(" perche' la vol trailing della gamba dipende anche dalle fee).")
print("-" * L)
print(f" {'lente':<40}{'cella scettico':>17}{'pos/n':>8}{'cella al buio':>17}{'pos/n':>8}")
FEEDEC = {}
keep_fee = V.FEE_RT
for fee, tag in ((keep_fee, f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)"),
(0.0007, "REALE 3,5 bps/lato = 7 bps RT"),
(0.0, "fee ZERO (informazione pura)")):
V.FEE_RT = fee
if fee == keep_fee:
EXf, basef = EX, base_leg
else:
EXf = {o: {a: V.extract(a, o) for a in ASSETS} for o in offs}
basef = {o: V.leg_daily(EXf[o], {a: V.size_flat(EXf[o][a]) for a in ASSETS})
for o in offs}
row = ""
for r in (RES[nmcan], CHOSEN):
fn = size_bin(r["thr"], r["w"], r["h"], r["sg"])
bk, ct = {}, {}
for o in offs:
lg = V.leg_daily(EXf[o], {a: fn(EXf[o][a], (tag, o, a)) for a in ASSETS})
bk[o], ct[o], _ = iso_pair(TP, lg, basef[o])
d = A.anchor_luck_delta(lambda o: bk[o], lambda o: ct[o], offs, metric=sh)
FEEDEC[(tag, cname(r["thr"], r["w"], r["h"], r["sg"]))] = d["median_paired"]
row += f"{d['median_paired']:>+17.3f}{d['n_positive']:>5}/{len(offs):<3}"
print(f" {tag:<40}{row}")
if fee != keep_fee:
del EXf, basef
gc.collect()
V.FEE_RT = keep_fee
assert V.FEE_RT == 0.001
print(f" (RSS {rss_mb():.0f} MB)")
f_mod = FEEDEC[(f"MODELLATA {keep_fee*1e4:.0f} bps RT (i backtest)", nmcan)]
f_zero = FEEDEC[("fee ZERO (informazione pura)", nmcan)]
quota_info = 100.0 * f_zero / f_mod if f_mod else float("nan")
print(f" -> a fee ZERO resta {f_zero:+.3f} di {f_mod:+.3f} = {quota_info:.0f}% dell'effetto.")
if quota_info >= 85.0:
print(" ❌ IPOTESI REFUTATA (era la mia, ed era anche quella del compito): il vantaggio")
print(" NON e' fee risparmiata. A fee zero l'effetto c'e' tutto, quindi scartare i")
print(" trade in alta vol e' INFORMAZIONE sul rendimento lordo, non un risparmio di")
print(" costo. Conseguenza pratica: NON si puo' replicare abbassando il turnover, e")
print(" NON va convertito con la curva fee del 26/07 (sarebbe doppio conteggio in una")
print(" direzione e attribuzione sbagliata nell'altra).")
else:
print(" Una parte e' fee risparmiata; la fee vera (3,5 bps/lato) e' piu' bassa di quella")
print(" modellata, quindi nel LIVE quella componente e' PIU' PICCOLA.")
# ---------------------------------------------------------- §10 trasferimento su V1
print("\n" + "-" * L)
print(" 10. TRASFERIMENTO su SKH01_V1 — seconda sequenza di trade, geometria d'uscita")
print(" DIVERSA (stop/target in ATR invece che a percentuale fissa). Non e' un hold-out")
print(" temporale, ma e' l'unico campione indipendente disponibile a questo costo.")
print("-" * L)
EX1 = {o: {a: V.extract(a, o, SKH01_V1) for a in ASSETS} for o in offs}
R.get5m.cache_clear() # i 5m non servono piu': ~80 MB restituiti
gc.collect()
base1 = {o: V.leg_daily(EX1[o], {a: V.size_flat(EX1[o][a]) for a in ASSETS}) for o in offs}
print(f" {'cella':<26}{'ISO dShFULL':>13}{'pos/n':>8}{'ISO dShHOLD':>14}{'pos/n':>8}"
f"{'tenuti':>9}")
for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)):
fn = size_bin(r["thr"], r["w"], r["h"], r["sg"])
b1, c1, kp = {}, {}, []
for o in offs:
lg = V.leg_daily(EX1[o], {a: fn(EX1[o][a], ("V1", o, a)) for a in ASSETS})
b1[o], c1[o], _ = iso_pair(TP, lg, base1[o])
kp.append(float(np.mean(np.concatenate(
[fn(EX1[o][a], ("V1", o, a)) for a in ASSETS]))))
dF = A.anchor_luck_delta(lambda o: b1[o], lambda o: c1[o], offs, metric=sh)
dH = A.anchor_luck_delta(lambda o: hold(b1[o]), lambda o: hold(c1[o]), offs, metric=sh)
print(f" {cname(r['thr'], r['w'], r['h'], r['sg']):<26}{dF['median_paired']:>+13.3f}"
f"{dF['n_positive']:>5}/{len(offs):<3}{dH['median_paired']:>+14.3f}"
f"{dH['n_positive']:>5}/{len(offs):<3}{float(np.median(kp))*100:>8.0f}%")
del EX1, base1
KC.clear()
gc.collect()
print(f" (RSS {rss_mb():.0f} MB)")
# ---------------------------------------------------------- §11 per anno
print("\n" + "-" * L)
print(" 11. SCOMPOSIZIONE PER ANNO (regola 22/08 dopo SOL: un contributo positivo si")
print(" scompone per ANNO prima di crederci — 24/24 ancore possono essere un anno solo)")
print("-" * L)
yrs = sorted({int(y) for y in BK[nmcan][can].index.year})
print(f" {'cella':<26}" + "".join(f"{y:>8}" for y in yrs))
for lab, r in (("cella dello scettico", RES[nmcan]), ("cella scelta al buio", CHOSEN)):
nm = cname(r["thr"], r["w"], r["h"], r["sg"])
cc = []
for y in yrs:
v = [sh(BK[nm][o][BK[nm][o].index.year == y]) - sh(CT[nm][o][CT[nm][o].index.year == y])
for o in offs if (BK[nm][o].index.year == y).sum() > 20]
cc.append(float(np.median(v)) if v else float("nan"))
print(f" {nm:<26}" + "".join(f"{c:>+8.2f}" for c in cc) +
f" positivi {int(np.nansum(np.asarray(cc) > 0))}/{len(yrs)}")
# ---------------------------------------------------------- §12 la domanda che decide
print("\n" + "-" * L)
print(" 12. LA DOMANDA CHE DECIDE — un effetto che vale un terzo della propria incertezza")
print(" merita di cambiare un sistema che gira con soldi veri?")
print("-" * L)
amp = max(ban_F) - min(ban_F)
nm = nmcan
dC = float(np.median([cagr(BK[nm][o]) - cagr(CT[nm][o]) for o in offs]))
cag_band = [cagr(book(TP, base_leg[o])) for o in offs]
print(f" (i) IL CONFRONTO GIUSTO NON E' QUELLO DELLA DOMANDA. La banda d'ancora del")
print(f" LIVELLO ({amp:.3f} di ampiezza, +-{amp/2:.3f}) e' incertezza sul livello, e")
print(f" si CANCELLA in un confronto APPAIATO: le due braccia girano sulla STESSA")
print(f" ancora. Cio' che va confrontato con l'effetto e' la dispersione della")
print(f" DIFFERENZA appaiata, che e' un ordine di grandezza piu' piccola.")
print(f" cella scettico: effetto {RES[nm]['dF']:+.3f}, banda appaiata "
f"[{RES[nm]['lo']:+.3f},{RES[nm]['hi']:+.3f}], "
f"{RES[nm]['nF']}/{len(offs)} ancore positive.")
print(f" (ii) MA la banda del livello torna a mordere DUE volte, e in modo decisivo:")
print(f" (a) la SCELTA della cella e' fatta guardando quella banda -> la stima onesta")
print(f" non e' l'argmax ma la MEDIANA DI FAMIGLIA "
f"({float(np.median([r['dF'] for r in POS])):+.3f} sul segno +1);")
print(f" (b) l'effetto e' misurato sugli STESSI 8 anni su cui SKH01 e' stato")
print(f" selezionato, e non esiste un forward capace di misurarlo (SE(Sharpe)")
print(f" ~1,4 su 6 mesi contro un effetto di 0,08).")
print(f" (iii) IL CRITERIO, dichiarato: si tocca il libro live solo se TUTTE E QUATTRO —")
print(f" (1) l'effetto sopravvive alla stima ONESTA di famiglia (mediana, non argmax)")
print(f" (2) `weights_tilt_null` passa (e' il gate codificato per i cambi di peso)")
print(f" (3) il gate marginale + un null CON POTENZA lo sostengono")
print(f" (4) l'effetto e' grande abbastanza da pagare un modo di fallire in piu'.")
print(f" (iv) ORDINI DI GRANDEZZA a ${CAPITAL:.0f} (regola 26/07: quando le leve hanno")
print(f" ordini di grandezza diversi, DIRLO):")
print(f" effetto della cella (ISO, mediana appaiata): dCAGR {dC*100:+.2f}%/anno")
print(f" = ${dC*CAPITAL:+.2f}/anno = EUR {dC*CAPITAL/365*0.86:+.4f}/giorno")
print(f" banda d'ancora del CAGR del libro: "
f"[{min(cag_band)*100:.1f}%, {max(cag_band)*100:.1f}%] "
f"= ${(max(cag_band)-min(cag_band))*CAPITAL:.0f}/anno di incertezza")
print(f" versare EUR 500/mese (il piano dell'operatore) = EUR 16,44/giorno di capitale")
print(f" nuovo, cioe' ~{16.44/max(1e-9, abs(dC*CAPITAL/365*0.86)):.0f}x l'effetto misurato qui.")
print(f" (v) IL CRITERIO APPLICATO, riga per riga. La forma quantitativa di (1) e'")
print(f" dichiarata qui: 'sopravvive alla stima di famiglia' = la MEDIANA della meta'")
print(f" d'ipotesi vale almeno la META' della cella selezionata (altrimenti non e' un")
print(f" plateau, e' una punta), E la quota di celle positive e' chiaramente sopra il")
print(f" 50% (altrimenti la famiglia e' una monetina). Soglie scelte per essere")
print(f" verificabili, non per far passare o bocciare un numero gia' visto.")
med_pos = float(np.median([r["dF"] for r in POS]))
fpos = sum(r["dF"] > 0 for r in POS) / len(POS)
c1 = (med_pos >= 0.5 * CHOSEN["dF"]) and (fpos >= 0.65)
eurday = dC * CAPITAL / 365 * 0.86
c4 = abs(eurday) >= 0.10
print(f" (1) mediana di famiglia (segno +1) {med_pos:+.3f} = "
f"{100*med_pos/CHOSEN['dF'] if CHOSEN['dF'] else float('nan'):.0f}% della cella scelta "
f"({CHOSEN['dF']:+.3f}); celle positive {fpos*100:.0f}% -> {'PASS' if c1 else 'FAIL'}")
print(f" (2) `weights_tilt_null`: gate_pass=False nella forma nuda, INERTE nella forma")
print(f" compensata -> non lo sostiene in nessuna delle due -> FAIL/vacuo")
print(f" (3) marginale ADDS su entrambe le celle + null di permutazione battuto a")
print(f" p<0.01 con la penale meccanica dentro (§6) -> PASS")
print(f" (4) taglia contro un modo di fallire in piu': EUR {eurday:+.4f}/giorno a "
f"${CAPITAL:.0f} -> {'PASS' if c4 else 'FAIL'} (soglia dichiarata: EUR 0,10/giorno)")
print(f" VERDETTO: {'CANDIDATO' if (c1 and c4) else 'LEAD / NESSUN CAMBIO'} — servono")
print(f" tutte e quattro; (1) e (4) falliscono, (2) non e' utilizzabile. Cio' che")
print(f" REGGE e va conservato e' il SEGNO: 0/72 celle positive sul controllo inverso.")
print("\n" + "=" * L)
print(f" fatto in {time.time()-t0:.0f}s")
print("=" * L)
if __name__ == "__main__":
main()
+957
View File
@@ -0,0 +1,957 @@
#!/usr/bin/env python
"""r0822b_bocpd.py — IL FALSIFICATORE NOMINATO E MAI ESEGUITO del filone ADAPTIVE-HORIZON.
CONTESTO (perche' questo file esiste)
-------------------------------------
Il 22/08 `r0822_adaptive_horizon.py` ha scartato il TSMOM a orizzonte adattivo con una diagnosi
netta: il "vincitore adattivo" aveva L_t **incollato al bordo il 100% del tempo** = un TSMOM a
orizzonte COSTANTE di 20 giorni travestito (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore);
isolando le 52 celle davvero adattive -> NEUTRAL, corr->TP01 0.90 = TP01 travestito.
Ma quell'agente ha dichiarato **cosa lo smentirebbe** e non ha potuto eseguirlo:
"Un rilevatore il cui L_t si muove DAVVERO e batte il miglior lookback COSTANTE (non TP01) in
>=6/8 anni appaiati con corr->TP01 < 0.6. La mia famiglia copre 3 rilevatori a soglia/finestra
fissa — un run-length posterior bayesiano (BOCPD/PELT vero) o un L scelto per walk-forward sul
PnL realizzato non sono stati provati."
Questo file esegue **quel** test. Oggi il filone e' chiuso su UNA famiglia (3 proxy a soglia
fissa), non sul MECCANISMO. Una seconda chiusura, con rilevatori veri, rende il "no" definitivo.
PERCHE' BOCPD E' STRUTTURALMENTE DIVERSO DAI 3 PROXY DEL PRIMO TENTATIVO
-----------------------------------------------------------------------
I 3 proxy (CUSUM a soglia k, variance-ratio, Hurst) producevano una STATISTICA che veniva poi
MAPPATA linearmente dentro [Lmin, Lmax]: la mappatura e' arbitraria, e infatti la statistica
saturava contro il clip (bordo 100%). Un run-length posterior NON ha bisogno di mappatura:
la lunghezza di run **e' gia' un lookback**, nelle stesse unita' (barre). "Quanto indietro posso
guardare" = "da quanto tempo non cambia il regime" e' un'identita', non una scelta di scala.
* BOCPD (Adams & MacKay 2007): posterior online su r_t con hazard costante e verosimiglianza
Normale-InverseGamma coniugata (predittiva Student-t). Causale PER COSTRUZIONE: e' un filtro
in avanti, ogni P(r_t | x_{1:t}) usa solo x_{1:t}.
* OP/PELT causale: PELT e' un'accelerazione (pruning) dell'**optimal partitioning**
F(t) = min_{s<t} [F(s) + C(y_{s+1:t}) + beta]. La ricorsione E' online. ⚠️ La maggior parte
delle implementazioni di change-point (ruptures.Pelt().fit_predict(intera_serie)) e'
**retrospettiva** e restituisce la segmentazione dell'INTERO campione = look-ahead colossale.
Qui F(t) e la sua backtracking-partition sono calcolati SOLO su y[0:t] per ogni t.
Verificato con `A.causality_ok` **e** con un troncamento manuale esplicito (sezione 0-bis).
* WF-PnL: L scelto per walk-forward sullo Sharpe NETTO realizzato dei candidati costanti nella
finestra all'indietro. Non usa alcun modello di regime: e' il "cosa avrebbe funzionato finora".
IL NULL GIUSTO NON E' TP01 — E' IL MIGLIOR LOOKBACK COSTANTE
------------------------------------------------------------
E' il controllo che ha ucciso il primo tentativo, e va rimesso PRIMA, non dopo: senza di esso
il null confonde "adattare paga" con "un orizzonte piu' corto paga su questo campione".
Percio' il confronto principale e': adattivo (cella scelta in-sample) vs miglior L COSTANTE
(cella scelta in-sample, stessa geometria, stesso TF, stesse fee), **appaiato per ANNO e per
ANCORA** (mediana delle DIFFERENZE APPAIATE, mai differenza delle mediane — lezione 26/07).
FAMIGLIA DICHIARATA **PRIMA** DI GUARDARE (conteggio al RIALZO)
--------------------------------------------------------------
A) rilevatori veri 2 (bocpd | op)
parametro 3 (bocpd: hazard lambda 50/100/250 g ; op: penalita' k 2/5/10)
statistica 2 (bocpd: E[r_t] | argmax r_t ; op: eta' dall'ultimo cp | mediana
delle lunghezze di segmento gia' concluse)
mappatura 2 (direct | inverse) [il verso non e' ovvio a priori => e' un asse]
blend 2 (single L | triple L/3,L,2L — con L=90 il triple E' TP01)
timeframe 2 (1d | 12h) [>=12h: sotto, costi+overfit dominano, 19/06]
= 2*3*2*2*2*2 = **96 trial**
B) controllo L COSTANTE 12 L x 2 blend x 2 TF = **48 trial**
C) WF-PnL (L da walk-forward) 3 finestre x 2 blend x 2 TF = **12 trial**
D) taratura della SCALA 8 penalita' OP + 5 hazard BOCPD = **13** (sezione 1-pre)
---------------------------------------------------------------- TOTALE DICHIARATO **169**
⚠️ (D) e' una scansione fatta guardando SOLO il numero di change-point rilevati — una proprieta'
del RILEVATORE — e mai uno Sharpe: nessuna di quelle 13 configurazioni produce un rendimento. Si
dichiara lo stesso, al rialzo, perche' ha ristretto la griglia di (A). Il suo esito e' gia' un
risultato: a penalita' OP >= 5 l'eta' resta incollata al tetto della finestra il 44% del tempo,
cioe' **l'artefatto del 22/08 riprodotto su un rilevatore vero** — e la griglia (A) usa solo la
regione in cui il rilevatore rileva davvero (0.5/1.0/2.0 = 18-40 change-point l'anno).
Costanti FISSATE A PRIORI (gradi di liberta' non cercati, dichiarati perche' esistono):
Lmin=20g, Lmax=200g (contengono 30..180 del canonico e sono gli stessi del primo tentativo);
BOCPD su rendimenti log STANDARDIZZATI da vol rolling causale 30g (il bersaglio e' il DRIFT, non
la vol); prior NIG mu0=0, kappa0=1, alpha0=1, beta0=1; troncamento del posterior a Lmax barre;
costo OP = mean-shift gaussiano su dati standardizzati, penalita' beta = k*log(t) causale.
Il deflated-Sharpe e' riportato a N=169 (mia famiglia), N=281 (mia + le 112 del primo tentativo:
**stessa domanda**, i trial si sommano) e N=449 (di SCREEN — lezione 22/08 del filone ORTHO:
su BTC/ETH direzionale il massimo atteso dal rumore su 168 trial e' Sharpe 1.572, SOPRA il
soffitto misurato ~1.3, quindi il conteggio di screen e' quello che decide).
ORDINE DI ESECUZIONE (imposto dalla missione, ed e' l'ordine giusto)
--------------------------------------------------------------------
La diagnostica che ha smascherato il primo tentativo va PER PRIMA: quanto si muove davvero L_t?
Se sta al bordo, l'artefatto e' riprodotto e il filone si chiude di nuovo senza guardare i
rendimenti. Solo se L_t si muove ha senso misurare se il movimento PAGA.
Runtime atteso ~6-10 min su 2 core. Nessun file toccato fuori da questo.
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.special import gammaln
from scipy.stats import norm
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT))
import altlib as A # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
ASSETS = A.CERTIFIED
HOLDOUT = A.HOLDOUT
# ---- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) -------------------------
LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte, IDENTICA al primo tentativo
FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> TP01 canonico
TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30
NIG_MU0, NIG_KAP0, NIG_AL0, NIG_BE0 = 0.0, 1.0, 1.0, 1.0
STD_WIN_D = 30 # finestra della standardizzazione causale dei rendimenti
TFS = ("1d", "12h")
DETECTORS = ("bocpd", "op")
PARAMS = {"bocpd": (50, 100, 250), "op": (0.5, 1.0, 2.0)}
# TARATURA DELLA SCALA (sezione 1-pre): scansione ESPLORATIVA fatta guardando SOLO il numero di
# change-point rilevati — una proprieta' del RILEVATORE — e mai lo Sharpe. Un rilevatore che non
# rileva nulla non e' un rilevatore: a pen_k>=5 l'eta' resta incollata al tetto della finestra
# (44% del tempo) = ESATTAMENTE l'artefatto del 22/08, riprodotto qui su un rilevatore vero.
CAL_PEN = (0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0)
CAL_LAM = (25, 50, 100, 250, 500)
STATS = {"bocpd": ("mean", "map"), "op": ("age", "segmed")}
MAPPINGS = ("direct", "inverse")
BLENDS = ("single", "triple")
CONST_L = (5, 7, 10, 15, 20, 30, 45, 60, 90, 120, 150, 200)
WF_WIN_D = (180, 365, 730)
N_DECLARED_A = len(DETECTORS) * 3 * 2 * len(MAPPINGS) * len(BLENDS) * len(TFS) # 96
N_DECLARED_B = len(CONST_L) * len(BLENDS) * len(TFS) # 48
N_DECLARED_C = len(WF_WIN_D) * len(BLENDS) * len(TFS) # 12
N_DECLARED_CAL = len(CAL_PEN) + len(CAL_LAM) # 13 (taratura)
N_DECLARED = N_DECLARED_A + N_DECLARED_B + N_DECLARED_C + N_DECLARED_CAL # 169
N_PRIOR_FAMILY = 112 # trial dichiarati da r0822_adaptive_horizon (stessa domanda)
N_SCREEN = 168 # pool di screen direzionale BTC/ETH dell'ondata (filone 12)
# =======================================================================================
# RILEVATORI VERI
# =======================================================================================
def _logret(c: np.ndarray) -> np.ndarray:
r = np.zeros(len(c))
r[1:] = np.log(c[1:] / c[:-1])
return r
def _std_returns(c: np.ndarray, bpd: int) -> np.ndarray:
"""Rendimenti log standardizzati da vol rolling CAUSALE (min_periods -> warmup a 0).
Il bersaglio del rilevatore e' il cambio di DRIFT: senza standardizzare, un cambio di
regime di VOLATILITA' dominerebbe la verosimiglianza e il rilevatore misurerebbe la vol."""
lr = _logret(c)
w = max(5, STD_WIN_D * bpd)
sig = pd.Series(lr).rolling(w, min_periods=max(5, w // 4)).std().values
y = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0)
return np.nan_to_num(np.clip(y, -8.0, 8.0))
def _student_logpdf(x: float, mu: np.ndarray, kap: np.ndarray,
al: np.ndarray, be: np.ndarray) -> np.ndarray:
"""Predittiva Student-t della Normale-InverseGamma coniugata (Murphy 2007, eq. 99-100):
x | mu,kappa,alpha,beta ~ t_{2 alpha}(mu, beta(kappa+1)/(alpha kappa))."""
nu = 2.0 * al
s2 = be * (kap + 1.0) / (al * kap)
z = (x - mu) ** 2 / (nu * s2)
return (gammaln(0.5 * (nu + 1.0)) - gammaln(0.5 * nu)
- 0.5 * np.log(nu * np.pi * s2) - 0.5 * (nu + 1.0) * np.log1p(z))
def _bocpd(y: np.ndarray, lam_bars: float, rmax: int) -> tuple[np.ndarray, np.ndarray]:
"""BOCPD (Adams & MacKay 2007) con hazard COSTANTE H = 1/lam_bars e verosimiglianza NIG.
Ritorna (E[r_t], argmax_r P(r_t)) in BARRE. Filtro in avanti: ogni valore usa solo il
passato -> causale per costruzione, e stabile su prefisso (nessuna statistica full-sample).
Il posterior e' troncato a rmax barre e rinormalizzato (il troncamento e' un limite di
memoria, NON un clip sul valore: se la massa si accumula a rmax lo si vede in diagnostica)."""
n = len(y)
haz = 1.0 / max(2.0, float(lam_bars))
R = np.array([1.0])
mu = np.array([NIG_MU0]); kap = np.array([NIG_KAP0])
al = np.array([NIG_AL0]); be = np.array([NIG_BE0])
rl = np.arange(1)
mean_rl = np.zeros(n); map_rl = np.zeros(n)
for t in range(n):
x = float(y[t])
lp = _student_logpdf(x, mu, kap, al, be)
lp -= lp.max()
pred = np.exp(lp)
gr = R * pred * (1.0 - haz)
cp = float(np.sum(R * pred * haz))
m_new = min(len(R) + 1, rmax + 1)
newR = np.empty(m_new)
newR[0] = cp
newR[1:] = gr[:m_new - 1]
s = newR.sum()
if not np.isfinite(s) or s <= 0:
newR = np.zeros(m_new); newR[0] = 1.0; s = 1.0
newR /= s
# aggiornamento delle statistiche sufficienti (run r -> run r+1), prior in testa
mu_f = np.concatenate(([NIG_MU0], (kap * mu + x) / (kap + 1.0)))[:m_new]
kap_f = np.concatenate(([NIG_KAP0], kap + 1.0))[:m_new]
al_f = np.concatenate(([NIG_AL0], al + 0.5))[:m_new]
be_f = np.concatenate(([NIG_BE0], be + kap * (x - mu) ** 2 / (2.0 * (kap + 1.0))))[:m_new]
R, mu, kap, al, be = newR, mu_f, kap_f, al_f, be_f
if len(rl) != m_new:
rl = np.arange(m_new, dtype=float)
mean_rl[t] = float(np.dot(R, rl))
map_rl[t] = float(rl[int(np.argmax(R))])
return mean_rl, map_rl
def _op_causal(y: np.ndarray, pen_k: float, smax: int) -> tuple[np.ndarray, np.ndarray]:
"""OPTIMAL PARTITIONING **online** (l'obiettivo esatto che PELT accelera con il pruning):
F(t) = min_{s in [t-smax, t-1]} [ F(s) + C(y[s:t]) + beta(t) ]
con C = costo gaussiano di mean-shift su dati standardizzati e beta(t) = pen_k*log(t).
⚠️ Il termine sum(y^2) del costo gaussiano e' INVARIANTE alla partizione (ogni punto sta in
esattamente un segmento) -> si omette dall'argmin. C(s:t) = -(S[t]-S[s])^2/(t-s).
Ritorna (eta' dall'ultimo change-point, mediana delle lunghezze dei segmenti CONCLUSI),
entrambe in barre. Per ogni t si usa SOLO y[0:t]: e' la differenza fra questo e un
`ruptures.Pelt().fit_predict(y)` retrospettivo, che guarda tutto il campione."""
n = len(y)
S = np.concatenate(([0.0], np.cumsum(y)))
F = np.full(n + 1, np.inf)
F[0] = 0.0
prev = np.zeros(n + 1, dtype=int)
age = np.zeros(n); segmed = np.zeros(n)
for t in range(1, n + 1):
a = max(0, t - smax)
s_idx = np.arange(a, t)
seg = (S[t] - S[a:t])
ln = (t - s_idx).astype(float)
cand = F[a:t] - seg * seg / ln
j = int(np.argmin(cand))
beta = pen_k * np.log(max(2.0, float(t)))
F[t] = cand[j] + beta
prev[t] = int(s_idx[j])
# backtracking della partizione VALIDA A t (cap a 40 segmenti: ci serve la mediana)
last = prev[t]
age[t - 1] = t - last
lens = []
u = last
while u > 0 and len(lens) < 40:
p = prev[u]
lens.append(u - p)
u = p
segmed[t - 1] = float(np.median(lens)) if lens else float(t)
return age, segmed
_STAT_CACHE: dict = {}
def _stat_key(df: pd.DataFrame, det: str, par) -> tuple:
"""Chiave chiusa sul CONTENUTO del frame (lunghezza + primo/ultimo timestamp): un PREFISSO
non deve prendere la cache del campione pieno, altrimenti `causality_ok` verrebbe ingannata
dalla cache invece che dal codice."""
ts = df["timestamp"].values
return (det, par, len(df), int(ts[0]), int(ts[-1]))
def detector_stats(df: pd.DataFrame, det: str, par) -> dict:
key = _stat_key(df, det, par)
if key in _STAT_CACHE:
return _STAT_CACHE[key]
bpd = A.bars_per_day(df)
c = df["close"].values.astype(float)
y = _std_returns(c, bpd)
rmax = max(4, LMAX_D * bpd)
if det == "bocpd":
m, mp = _bocpd(y, float(par) * bpd, rmax)
out = {"mean": m, "map": mp}
elif det == "op":
ag, sm = _op_causal(y, float(par), rmax)
out = {"age": ag, "segmed": sm}
else:
raise ValueError(det)
_STAT_CACHE[key] = out
return out
def lookback_bars(df: pd.DataFrame, det: str, par, stat: str, mapping: str) -> np.ndarray:
"""L_t in BARRE dentro [Lmin, Lmax] giorni. `direct` = la lunghezza di run E' il lookback
(dopo un change-point si puo' guardare indietro solo fin la'); `inverse` e' lo specchio,
dichiarato come asse perche' il verso non e' ovvio a priori."""
bpd = A.bars_per_day(df)
lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd)
v = detector_stats(df, det, par)[stat]
base = np.clip(np.nan_to_num(v, nan=float(lo)), lo, hi)
L = base if mapping == "direct" else (lo + hi - base)
return np.clip(np.round(L), 2, hi).astype(int)
# =======================================================================================
# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target — IDENTICA al primo tentativo
# =======================================================================================
def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
n = len(c)
i = np.arange(n)
j = i - h
ok = j >= 0
s = np.zeros(n)
jj = np.where(ok, j, 0)
s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0)
return s, ok
def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray:
hs = [L] if blend == "single" else [
np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)]
acc = np.zeros(len(c)); cnt = np.zeros(len(c))
for h in hs:
s, ok = _sign_at(c, np.asarray(h, int))
acc[ok] += s[ok]; cnt[ok] += 1
out = np.zeros(len(c)); nz = cnt > 0
out[nz] = acc[nz] / cnt[nz]
return out
def fixed_target(df: pd.DataFrame) -> np.ndarray:
"""BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico."""
c = df["close"].values.astype(float)
bpd = A.bars_per_day(df)
acc = np.zeros(len(c)); cnt = np.zeros(len(c))
for hd in FIXED_H:
s, ok = _sign_at(c, np.full(len(c), hd * bpd, int))
acc[ok] += s[ok]; cnt[ok] += 1
d = np.zeros(len(c)); nz = cnt > 0
d[nz] = acc[nz] / cnt[nz]
return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV)
def const_factory(tf: str, L: int, blend: str):
def fn(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
bpd = A.bars_per_day(df)
Lb = np.full(len(c), max(2, int(round(L * bpd))), int)
return A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV)
fn.__name__ = f"const_L{L}_{blend}"
return fn
def factory(tf: str, det: str, par, stat: str, mapping: str, blend: str):
def fn(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
L = lookback_bars(df, det, par, stat, mapping)
return A.vol_target(np.clip(_dir_from_L(c, L, blend), 0, None), df, TGT_VOL, VOLWIN, LEV)
fn.__name__ = f"adapt_{det}{par}_{stat}_{mapping}_{blend}"
return fn
_WF_CACHE: dict = {}
def wf_picks(df: pd.DataFrame, win_d: int, blend: str):
"""(C) L scelto per WALK-FORWARD sul PnL NETTO realizzato: a ogni barra si prende l'L che,
fra i candidati COSTANTI, ha lo Sharpe netto migliore nella finestra all'indietro `win_d`.
Nessun modello di regime: e' il "cosa avrebbe funzionato finora".
Causale: il PnL della barra i si conosce solo a fine barra i, quindi per decidere a i si usa
la finestra che finisce a i-1; `eval_weights` shifta poi la posizione di un'altra barra."""
cands = [L for L in CONST_L if LMIN_D <= L <= LMAX_D]
key = (win_d, blend) + _stat_key(df, "wf", 0)[2:]
if key in _WF_CACHE:
return _WF_CACHE[key]
c = df["close"].values.astype(float)
bpd = A.bars_per_day(df)
n = len(c)
r = A.simple_returns(c)
w = max(20, win_d * bpd)
tgts, sh = [], []
for L in cands:
Lb = np.full(n, max(2, int(round(L * bpd))), int)
t_ = A.vol_target(np.clip(_dir_from_L(c, Lb, blend), 0, None), df, TGT_VOL, VOLWIN, LEV)
pos = np.zeros(n); pos[1:] = t_[:-1]
net = pos * r - A.FEE_SIDE * np.abs(np.diff(pos, prepend=0.0))
ss = pd.Series(net)
mu = ss.rolling(w, min_periods=w // 2).mean()
sd = ss.rolling(w, min_periods=w // 2).std()
with np.errstate(invalid="ignore", divide="ignore"):
sr = (mu / sd).values
sr = np.concatenate(([np.nan], sr[:-1])) # PnL noto solo a fine barra
tgts.append(t_); sh.append(np.where(np.isfinite(sr), sr, -np.inf))
SH = np.vstack(sh); TG = np.vstack(tgts)
idx = np.argmax(SH, axis=0)
idx[~np.isfinite(SH).any(axis=0)] = cands.index(90) # warmup -> il canonico, non il migliore
out = (np.asarray([cands[i] for i in idx]), TG[idx, np.arange(n)], cands)
_WF_CACHE[key] = out
return out
def wf_factory(tf: str, win_d: int, blend: str):
def fn(df: pd.DataFrame) -> np.ndarray:
return wf_picks(df, win_d, blend)[1]
fn.__name__ = f"wfpnl_{win_d}_{blend}"
return fn
GRID_A = [dict(det=d, par=p, stat=s, mapping=m, blend=b)
for d in DETECTORS for p in PARAMS[d] for s in STATS[d]
for m in MAPPINGS for b in BLENDS]
GRID_B = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS]
GRID_C = [dict(win_d=w, blend=b) for w in WF_WIN_D for b in BLENDS]
# =======================================================================================
# UTILITA'
# =======================================================================================
def _dd(s: pd.Series) -> float:
eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float))
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def _cagr(s: pd.Series) -> float:
s = s.dropna()
if len(s) < 5:
return float("nan")
eq = float(np.prod(1.0 + s.values))
yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25)
return eq ** (1 / max(yrs, 1e-6)) - 1.0 if eq > 0 else -1.0
def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame:
"""Stessa convenzione di trend_portfolio.resample_tf, ancora spostata di `off` ore.
NB `offset=`, non `origin=` (pandas 2.x ignora origin in silenzio — lezione 02/07)."""
g = df_1h.copy()
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
idx.name = "dt"
g.index = idx
out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
out = out.dropna(subset=["open"])
out["datetime"] = out.index
epoch = pd.Timestamp("1970-01-01", tz="UTC")
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return out.reset_index(drop=True)[
["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]:
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
rows = []
for y, g in J.groupby(J.index.year):
if len(g) < 40:
continue
rows.append(dict(year=int(y), n=len(g),
sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2),
d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2)))
return rows
def delever_null(cand: pd.Series, base: pd.Series) -> dict:
"""Esiste k<1 che da' al BASELINE lo stesso maxDD del candidato con Sharpe >= ?
Se si', il candidato non ha comprato protezione: ha solo tolto leva."""
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
dd_c, dd_b = _dd(J["C"]), _dd(J["B"])
if dd_c >= dd_b or dd_b <= 0:
return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
nota="il candidato NON riduce il maxDD: il null non si applica")
lo, hi = 0.0, 1.0
for _ in range(60):
k = 0.5 * (lo + hi)
if _dd(k * J["B"]) < dd_c:
lo = k
else:
hi = k
k = 0.5 * (lo + hi)
return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
sharpe_cand=round(A._sh(J["C"]), 3),
sharpe_base_delev=round(A._sh(k * J["B"]), 3),
superato=bool(A._sh(J["C"]) > A._sh(k * J["B"])))
_ANCHOR_CACHE: dict = {}
def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series:
key = (name, hours, off)
if key not in _ANCHOR_CACHE:
ser = {}
for a in ASSETS:
dfo = resample_offset(A.get(a, "1h"), hours, off)
ev = A.eval_weights(dfo, A._call_target(fn, dfo, a))
ser[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
_ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
return _ANCHOR_CACHE[key]
def adaptivity(tf: str, det: str, par, stat: str, mapping: str) -> dict:
"""LA DIAGNOSTICA CHE HA SMASCHERATO IL PRIMO TENTATIVO — quanto si MUOVE davvero L_t.
Criterio (identico a quello del 22/08, per confrontabilita'): sta a un bordo <50% del tempo
E l'IQR e' >= 10 giorni. In piu': |dL|/giorno e numero di RESET (cadute di L > 5 giorni),
che sono la firma di un vero rilevatore di change-point."""
fl, fh, iq, md, dl, nrs, sat = [], [], [], [], [], [], []
for a in ASSETS:
df = A.get(a, tf)
bpd = A.bars_per_day(df)
L = lookback_bars(df, det, par, stat, mapping) / bpd
raw = np.nan_to_num(detector_stats(df, det, par)[stat], nan=0.0) / bpd
fl.append(float(np.mean(L <= LMIN_D + 1)))
fh.append(float(np.mean(L >= LMAX_D - 1)))
iq.append(float(np.percentile(L, 75) - np.percentile(L, 25)))
md.append(float(np.median(L)))
dl.append(float(np.median(np.abs(np.diff(L)))))
d = np.diff(L)
nrs.append(float(np.sum(d < -5.0) / (len(L) / (365.25)))) # reset all'anno
sat.append(float(np.mean(raw >= LMAX_D - 1))) # saturazione del GREZZO
return dict(frac_lo=float(np.mean(fl)), frac_hi=float(np.mean(fh)),
iqr_days=float(np.mean(iq)), med_days=float(np.mean(md)),
dL_med=float(np.mean(dl)), reset_yr=float(np.mean(nrs)),
sat_raw=float(np.mean(sat)),
adattiva=bool(np.mean(fl) < 0.5 and np.mean(fh) < 0.5 and np.mean(iq) >= 10.0))
def hr(t: str = "") -> None:
print("\n" + "=" * 86)
if t:
print(t)
print("=" * 86)
def dsr_given_sr0(daily: pd.Series, sr_ann: float, sr0_ann: float,
dpy: float = 365.25) -> float:
"""Deflated-Sharpe con il massimo-atteso-dal-rumore IMPOSTO dall'esterno.
Serve perche' `A.deflated_sharpe` stima sr0 dalla VARIANZA degli Sharpe della griglia: su
una famiglia OMOGENEA (qui sono tutte varianti di TSMOM, quindi molto correlate) quella
varianza e' piccola, sr0 esce basso e il DSR esce alto **per costruzione**. Il conteggio che
decide, sulla ricerca direzionale BTC/ETH, e' quello di SCREEN: il filone ORTHO ha misurato
che su 168 trial il massimo atteso dal puro rumore e' Sharpe 1.572."""
r = np.asarray(pd.Series(daily).dropna().values, float)
T = len(r)
sr, sr0 = sr_ann / np.sqrt(dpy), sr0_ann / np.sqrt(dpy)
sk = float(pd.Series(r).skew())
ku = float(pd.Series(r).kurt()) + 3.0
den = np.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2))
return float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den))
def dsr_line(sr: float, allsr: list, daily: pd.Series, label: str, n_decl: int) -> str:
pad = list(allsr) + [float(np.mean(allsr))] * max(0, n_decl - len(allsr))
d, s0 = A.deflated_sharpe(sr, pad, daily)
return (f" N={n_decl:>4} ({label:<24}) DSR={d:.3f} {'PASS' if d >= 0.95 else 'FAIL'}"
f" max atteso dal rumore {s0:+.3f}")
# =======================================================================================
def main() -> None:
t0 = time.time()
print(__doc__.split("Runtime atteso")[0])
# -- 0. SANITY -----------------------------------------------------------------------
hr("0. SANITY — il baseline 'fisso' e' TP01 canonico (se non lo e', ogni delta e' inventato)")
tp = TrendPortfolio(**CANONICAL)
worst = 0.0
for a in ASSETS:
df = A.get(a, "1d")
worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df)))))
base_1d = A.candidate_daily(fixed_target, tf="1d")
tp01 = A.tp01_baseline_daily()
JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna()
print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}")
print(f" max|serie_giornaliera_mia - A.tp01_baseline_daily()| = "
f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})")
assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui"
# -- 0-bis. IL RILEVATORE E' CAUSALE? (troncamento MANUALE, prima dei gate) -----------
hr("0-bis. TRONCAMENTO MANUALE — la trappola del change-point RETROSPETTIVO")
print(" Un `Pelt().fit_predict(serie_intera)` restituisce la segmentazione dell'INTERO")
print(" campione: al tempo t 'saprebbe' dei cambi futuri. Qui BOCPD e OP sono ricorsioni in")
print(" avanti. Prova diretta: ricalcolo su un PREFISSO e confronto la coda con il pieno.")
for det, par in (("bocpd", 100), ("op", 5.0)):
for stat in STATS[det]:
wmax = 0.0
for a in ASSETS:
df = A.get(a, "1d")
full = lookback_bars(df, det, par, stat, "direct")
for frac in (0.70, 0.85):
cut = int(len(df) * frac)
sub = df.iloc[:cut].reset_index(drop=True)
pre = lookback_bars(sub, det, par, stat, "direct")
wmax = max(wmax, float(np.max(np.abs(pre[-100:] - full[cut - 100:cut]))))
print(f" {det:<6} {str(par):>5} {stat:<7}: max|L(prefisso)-L(pieno)| sulla coda "
f"= {wmax:.3e} barre -> {'CAUSALE' if wmax < 1e-9 else 'SOSPETTO'}")
# -- 1-pre. TARATURA DELLA SCALA — guardando il RILEVATORE, mai lo Sharpe -----------
hr("1-pre. TARATURA DELLA SCALA DEI PARAMETRI (guardando SOLO quanti change-point rileva)")
print(" Un rilevatore che non rileva nulla non e' un rilevatore. Questa scansione guarda il")
print(" NUMERO di change-point e la distribuzione di L_t — mai un rendimento. Dichiarata")
print(" come 13 trial in piu' (al rialzo), benche' nessuno produca uno Sharpe.")
dfc = A.get("BTC", "1d")
yc = _std_returns(dfc["close"].values.astype(float), 1)
print(f"\n OP (BTC 1d) {'pen_k':>6} {'cp/anno':>8} {'age med':>8} {'IQR':>7} {'@tetto':>8}")
for k in CAL_PEN:
ag, _ = _op_causal(yc, k, LMAX_D)
ncp = int(np.sum(np.diff(ag) < 0))
flag = " <= USATO" if k in PARAMS["op"] else (" <= degenere (artefatto 22/08)"
if np.mean(ag >= LMAX_D - 1) > 0.30 else "")
print(f" {k:>6} {ncp/(len(yc)/365.25):>8.1f} {np.median(ag):>8.1f} "
f"{np.percentile(ag,75)-np.percentile(ag,25):>7.1f} "
f"{np.mean(ag>=LMAX_D-1)*100:>7.1f}%{flag}")
print(f"\n BOCPD (BTC 1d) {'lambda':>6} {'E[r] med':>9} {'IQR':>7} {'@tetto':>8} "
f"{'MAP med':>8} {'@tetto':>8}")
for lam in CAL_LAM:
mn, mp = _bocpd(yc, float(lam), LMAX_D)
flag = " <= USATO" if lam in PARAMS["bocpd"] else ""
print(f" {lam:>6} {np.median(mn):>9.1f} "
f"{np.percentile(mn,75)-np.percentile(mn,25):>7.1f} "
f"{np.mean(mn>=LMAX_D-1)*100:>7.1f}% {np.median(mp):>8.1f} "
f"{np.mean(mp>=LMAX_D-1)*100:>7.1f}%{flag}")
# -- 1. LA DIAGNOSTICA DI MECCANISMO, PER PRIMA --------------------------------------
hr("1. L_t SI MUOVE DAVVERO? (se sta al bordo l'artefatto e' riprodotto: si chiude e basta)")
print(" criterio (identico al 22/08, per confrontabilita'): a un bordo <50% del tempo E")
print(" IQR >= 10 giorni. `reset/anno` = cadute di L > 5 giorni = firma di un change-point.")
print(f"\n {'TF':>4} {'det':>6} {'par':>5} {'stat':>7} {'map':<7} {'bordo-lo':>9} "
f"{'bordo-hi':>9} {'IQR(g)':>7} {'med(g)':>7} {'|dL|(g)':>8} {'reset/a':>8} "
f"{'sat.grezzo':>10} esito")
diag: dict = {}
for tf in TFS:
for d in DETECTORS:
for p in PARAMS[d]:
for st in STATS[d]:
for mp in MAPPINGS:
v = adaptivity(tf, d, p, st, mp)
diag[(tf, d, p, st, mp)] = v
print(f" {tf:>4} {d:>6} {str(p):>5} {st:>7} {mp:<7} "
f"{v['frac_lo']*100:8.1f}% {v['frac_hi']*100:8.1f}% "
f"{v['iqr_days']:7.1f} {v['med_days']:7.1f} {v['dL_med']:8.2f} "
f"{v['reset_yr']:8.1f} {v['sat_raw']*100:9.1f}% "
f"{'ADATTIVA' if v['adattiva'] else 'costante-travestito'}")
n_ad = sum(1 for v in diag.values() if v["adattiva"])
print(f"\n configurazioni di rilevatore DAVVERO adattive: {n_ad}/{len(diag)}")
if n_ad == 0:
hr("STOP — ARTEFATTO RIPRODOTTO")
print(" Nessun rilevatore muove L_t: ogni 'vincitore' sarebbe un orizzonte COSTANTE")
print(" travestito, esattamente come il 22/08. Il filone si chiude per la seconda volta")
print(" senza bisogno di guardare i rendimenti. FINE.")
return
# -- 2. GRIGLIA (A): cella scelta IN-SAMPLE + deflated-Sharpe -------------------------
hr(f"2. study_family_honest (A) — {len(GRID_A)} celle x {len(TFS)} TF = "
f"{len(GRID_A)*len(TFS)} trial")
rep = A.study_family_honest("BOCPD-H", factory, GRID_A, TFS)
ch = rep["chosen"]
print(f" celle valutate : {rep['n_cells']}")
print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']}")
print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}")
print(f" deflated-Sharpe (N={rep['n_cells']}): {rep['deflated_sharpe']} "
f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}")
print(f" earns_slot_marginal : {rep['earns_slot_marginal']}")
print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}")
print("\n top-10 IN-SAMPLE (come si sceglie onestamente):")
for r in rep["rows"][:10]:
ad = diag[(r["tf"], r["params"]["det"], r["params"]["par"],
r["params"]["stat"], r["params"]["mapping"])]["adattiva"]
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
f"{r['params']} {'[adattiva]' if ad else '[costante-travestito]'}")
fn_ch = factory(tf=ch["tf"], **ch["params"])
d_ch = A.candidate_daily(fn_ch, tf=ch["tf"])
# -- 3. CONTROLLO (B): il MIGLIOR ORIZZONTE COSTANTE ---------------------------------
hr(f"3. CONTROLLO L COSTANTE (B) — {len(GRID_B)} celle x {len(TFS)} TF = "
f"{len(GRID_B)*len(TFS)} trial [IL NULL GIUSTO, non TP01]")
csel = A.select_cell_insample(const_factory, GRID_B, TFS)
cch = csel["chosen"]
print(f" miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} "
f"IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}")
print(" top-8 costanti in-sample (L=90 triple 1d == TP01 canonico):")
for r in csel["rows"][:8]:
tag = " <== TP01" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple"
and r["tf"] == "1d") else ""
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}")
fn_const = const_factory(tf=cch["tf"], **cch["params"])
d_const = A.candidate_daily(fn_const, tf=cch["tf"])
# -- 4. WF-PnL (C) --------------------------------------------------------------------
hr(f"4. WALK-FORWARD SUL PnL REALIZZATO (C) — {len(GRID_C)} celle x {len(TFS)} TF = "
f"{len(GRID_C)*len(TFS)} trial")
wsel = A.select_cell_insample(wf_factory, GRID_C, TFS)
wch = wsel["chosen"]
print(f" miglior WF scelto in-sample: tf={wch['tf']} {wch['params']} "
f"IS {wch['insample_sharpe']} FULL {wch['full_sharpe']}")
for r in wsel["rows"]:
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
f"{r['params']}")
fn_wf = wf_factory(tf=wch["tf"], **wch["params"])
d_wf = A.candidate_daily(fn_wf, tf=wch["tf"])
# quanto si muove L nella variante WF? (stesso metro della sezione 1)
for a in ASSETS:
df = A.get(a, wch["tf"]); bpd = A.bars_per_day(df)
pick, _, cands = wf_picks(df, **wch["params"])
print(f" {a}: L_WF giorni mediana {np.median(pick):.0f} IQR "
f"{np.percentile(pick,75)-np.percentile(pick,25):.0f} "
f"bordo basso {np.mean(pick<=min(cands))*100:.1f}% "
f"bordo alto {np.mean(pick>=max(cands))*100:.1f}% "
f"cambi/anno {np.sum(np.diff(pick)!=0)/(len(pick)/(bpd*365.25)):.0f}")
# -- 5. IL CONFRONTO CHE DECIDE ------------------------------------------------------
hr("5. ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti IN-SAMPLE) — e vs TP01")
base_ch = A.candidate_daily(fixed_target, tf=ch["tf"])
for nome, ser in (("BOCPD/OP (A)", d_ch), ("WF-PnL (C)", d_wf),
("miglior COSTANTE (B)", d_const), ("TP01 30/90/180", base_1d)):
h = ser[ser.index >= HOLDOUT]
print(f" {nome:<22} FULL Sh {A._sh(ser):+.3f} HOLD Sh {A._sh(h):+.3f} "
f"maxDD {_dd(ser)*100:5.2f}% CAGR {_cagr(ser)*100:6.2f}% "
f"corr->TP01 {pd.concat({'a':ser,'b':tp01},axis=1,join='inner').dropna().corr().iloc[0,1]:+.3f}")
for lab, cand, ref, refname in (("A vs B", d_ch, d_const, "miglior costante"),
("C vs B", d_wf, d_const, "miglior costante"),
("A vs TP01", d_ch, base_1d, "TP01")):
J = pd.concat({"C": cand, "B": ref}, axis=1, join="inner").dropna()
JH = J[J.index >= HOLDOUT]
rows = paired_by_year(cand, ref)
ds = [r["d_sh"] for r in rows]
print(f"\n --- {lab} ({refname}) --- corr {J['C'].corr(J['B']):+.3f}")
print(f" dSh FULL {A._sh(J['C'])-A._sh(J['B']):+.3f} "
f"dSh HOLD {A._sh(JH['C'])-A._sh(JH['B']):+.3f}")
print(" differenza APPAIATA per anno:")
print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in rows))
print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} "
f"mediana {np.median(ds):+.2f} media {np.mean(ds):+.2f} "
f"[criterio del falsificatore: >=6/8 anni]")
# decomposizione: quanto del vantaggio su TP01 e' ADATTIVITA' e quanto e' "L piu' corto"?
JT = pd.concat({"C": d_ch, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna()
v_tp = A._sh(JT["C"]) - A._sh(JT["B"])
v_ct = A._sh(JT["K"]) - A._sh(JT["B"])
print(f"\n DECOMPOSIZIONE del vantaggio dell'adattivo su TP01 (FULL, finestra comune):")
print(f" adattivo - TP01 = {v_tp:+.3f}")
print(f" miglior COSTANTE - TP01 = {v_ct:+.3f} <- quota spiegata da 'un L piu' corto'")
print(f" adattivo - miglior COSTANTE= {v_tp - v_ct:+.3f} <- quota spiegata dall'ADATTARE")
print(f" => l'adattivita' spiega il {100*(v_tp-v_ct)/v_tp if abs(v_tp)>1e-9 else float('nan'):+.0f}% "
f"del vantaggio su TP01")
# la superficie di L costante e' una CURVA o un CRINALE DI RUMORE? (replica del 22/08)
rows12 = {r["params"]["L"]: r["insample_sharpe"] for r in csel["rows"]
if r["tf"] == cch["tf"] and r["params"]["blend"] == cch["params"]["blend"]}
Ls = sorted(rows12)
adj = float(np.median(np.abs(np.diff([rows12[L] for L in Ls]))))
print(f"\n RISOLUZIONE della griglia costante ({cch['tf']}, {cch['params']['blend']}): salto IS "
f"mediano fra L ADIACENTI {adj:+.3f}")
print(f" contro il vantaggio del miglior costante su TP01 ({v_ct:+.3f}) -> rapporto "
f"{adj/max(1e-9, abs(v_ct)):.2f} [replica del 'crinale di rumore' del 22/08]")
# -- 6. GATE --------------------------------------------------------------------------
hr("6. GATE sul candidato (A) scelto in-sample")
print(A.fmt_marginal(rep["marginal"]))
cz = A.causality_ok(fn_ch, tf=ch["tf"])
print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} "
f"(prefissi controllati {cz['checked']})")
imp = A.implausible_sharpe(d_ch)
print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} "
f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} "
f"attive {imp.get('active_frac')}")
for rr in imp["reasons"]:
print(f" - {rr}")
print(f" null de-levering vs COSTANTE : {delever_null(d_ch, d_const)}")
print(f" null de-levering vs TP01 : {delever_null(d_ch, base_1d)}")
print(" haircut a $600 (min-order $5):")
for a in ASSETS:
df = A.get(a, ch["tf"])
sc = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0)
print(f" {a}: modellato {sc['modeled']['sharpe']:+.3f} -> reale "
f"{sc['realistic']['sharpe']:+.3f} (haircut {sc['sharpe_haircut']:+.3f}, "
f"{sc['n_executed_trades']} trade)")
# DSR a piu' conteggi: famiglia, famiglia+112 del primo tentativo, di SCREEN
all_sr = ([r["full_sharpe"] for r in rep["rows"]]
+ [r["full_sharpe"] for r in csel["rows"]]
+ [r["full_sharpe"] for r in wsel["rows"]])
hr("6-bis. DEFLATED-SHARPE — sensibilita' del verdetto al CONTEGGIO (regola 30/07 + 22/08)")
print(f" Sharpe FULL del candidato (A) = {A._sh(d_ch):+.3f}")
print(dsr_line(A._sh(d_ch), all_sr, d_ch, "mia famiglia A+B+C", N_DECLARED))
print(dsr_line(A._sh(d_ch), all_sr, d_ch, "+ le 112 del 22/08", N_DECLARED + N_PRIOR_FAMILY))
print(dsr_line(A._sh(d_ch), all_sr, d_ch, "di SCREEN (ondata)",
N_DECLARED + N_PRIOR_FAMILY + N_SCREEN))
print("\n ⚠️ QUEL 'PASS' NON CERTIFICA NIENTE, e va detto: `deflated_sharpe` stima il massimo")
print(" atteso dal rumore dalla VARIANZA degli Sharpe della griglia. Qui la griglia e'")
print(" OMOGENEA (tutte varianti di TSMOM, molto correlate fra loro) -> varianza piccola")
print(" -> sr0 basso -> DSR alto per COSTRUZIONE. Aumentare N padding con la media alza N")
print(" ma non la varianza. Il conteggio che decide e' quello di SCREEN, e il filone ORTHO")
print(" lo ha gia' misurato: su 168 trial di ricerca direzionale BTC/ETH il massimo atteso")
print(" dal puro rumore e' Sharpe 1.572. Riscritto con QUEL sr0:")
for nome, ser in (("adattivo (A)", d_ch), ("miglior costante (B)", d_const),
("TP01 30/90/180", base_1d)):
print(f" {nome:<22} Sharpe FULL {A._sh(ser):+.3f} vs sr0_screen 1.572 -> "
f"DSR {dsr_given_sr0(ser, A._sh(ser), 1.572):.3f} "
f"{'PASS' if dsr_given_sr0(ser, A._sh(ser), 1.572) >= 0.95 else 'FAIL'}")
# PROVA DI IDENTITA': la cella "adattiva" al 100% sul bordo E' il costante, bit per bit
hr("6-ter. PROVA DI IDENTITA' — la cella al bordo NON somiglia a un costante: LO E'")
deg = [r for r in rep["rows"]
if diag[(r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["stat"],
r["params"]["mapping"])]["frac_lo"] >= 0.999]
if deg:
dr = max(deg, key=lambda r: r["insample_sharpe"])
s_deg = A.candidate_daily(factory(tf=dr["tf"], **dr["params"]), tf=dr["tf"])
s_con = A.candidate_daily(const_factory(tf=dr["tf"], L=LMIN_D,
blend=dr["params"]["blend"]), tf=dr["tf"])
Jd = pd.concat({"a": s_deg, "b": s_con}, axis=1, join="inner").dropna()
print(f" cella 100% al bordo basso: {dr['tf']} {dr['params']} IS {dr['insample_sharpe']}"
f" FULL {dr['full_sharpe']}")
print(f" costante L={LMIN_D}g {dr['params']['blend']} {dr['tf']}: "
f"FULL {A._sh(s_con):+.3f}")
print(f" max|serie_adattiva - serie_costante| = {float(np.max(np.abs(Jd['a']-Jd['b']))):.3e}"
f" (n={len(Jd)}) -> {'SONO LA STESSA STRATEGIA' if float(np.max(np.abs(Jd['a']-Jd['b']))) < 1e-12 else 'diverse'}")
else:
print(" nessuna cella e' al 100% sul bordo basso (la prova non si applica)")
# -- 7. FAMIGLIA RISTRETTA ALLE SOLE CELLE DAVVERO ADATTIVE --------------------------
hr("7. GATE SULLA SOLA FAMIGLIA DAVVERO ADATTIVA (la domanda della missione, isolata)")
ad_rows = [r for r in rep["rows"]
if diag[(r["tf"], r["params"]["det"], r["params"]["par"],
r["params"]["stat"], r["params"]["mapping"])]["adattiva"]]
d_ad_keep = fn_ad_keep = best_keep = None
if not ad_rows:
print(" NESSUNA cella della famiglia e' davvero adattiva -> vedi sezione 1.")
else:
best = max(ad_rows, key=lambda r: r["insample_sharpe"])
print(f" celle adattive: {len(ad_rows)}/{len(rep['rows'])} miglior IN-SAMPLE: "
f"tf={best['tf']} {best['params']} IS {best['insample_sharpe']} "
f"FULL {best['full_sharpe']}")
fn_ad = factory(tf=best["tf"], **best["params"])
d_ad = A.candidate_daily(fn_ad, tf=best["tf"])
sm = A.study_marginal("BOCPD-H (sole adattive)", fn_ad, tf=best["tf"])
dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad),
[r["full_sharpe"] for r in ad_rows], d_ad)
print(f" marginal={sm['marginal_verdict']} earns_slot={sm['earns_slot']} "
f"corr->TP01 {sm['marginal'].get('corr_full')}")
print(f" deflated-Sharpe (N={len(ad_rows)}) = {dsr_ad:.3f} "
f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:+.3f})")
JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna()
JH = JA[JA.index >= HOLDOUT]
print(f" FULL adattiva-vera {A._sh(JA['C']):+.3f} | costante {A._sh(JA['K']):+.3f} "
f"| TP01 {A._sh(JA['B']):+.3f}")
print(f" HOLD-OUT adattiva-vera {A._sh(JH['C']):+.3f} | costante {A._sh(JH['K']):+.3f} "
f"| TP01 {A._sh(JH['B']):+.3f}")
r3 = paired_by_year(d_ad, d_const)
d3 = [r["d_sh"] for r in r3]
print(" vs miglior costante, differenza APPAIATA per anno:")
print(" " + " ".join(f"{r['year']}:{r['d_sh']:+.2f}" for r in r3))
print(f" -> dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)} mediana {np.median(d3):+.2f}"
f" corr al costante {JA['C'].corr(JA['K']):+.3f}")
print(" ⚠️ l'hold-out sono gli ULTIMI DUE anni di questa stessa riga: se il vantaggio")
print(" hold-out sta li' e non nel resto, e' un evento, non un meccanismo (regola:")
print(" un vantaggio concentrato in un anno non e' un vantaggio).")
print(f" null de-levering vs costante: {delever_null(d_ad, d_const)}")
d_ad_keep, fn_ad_keep, best_keep = d_ad, fn_ad, best
# -- 8. BANDA D'ANCORA (delta APPAIATO adattivo - costante) --------------------------
hours = 24 if ch["tf"] == "1d" else 12
offs = list(range(hours))
hr(f"8. BANDA D'ANCORA — {len(offs)} ancore. Stima onesta = MEDIANA; il confronto fra due "
f"varianti\n e' la MEDIANA DELLE DIFFERENZE APPAIATE (mai la differenza delle mediane).")
if cch["tf"] != ch["tf"]:
print(f" ⚠️ adattivo su {ch['tf']} e miglior costante su {cch['tf']}: per il delta "
f"appaiato uso il miglior costante RIVALUTATO su {ch['tf']} (stesse ancore = coppie).")
crow = max([r for r in csel["rows"] if r["tf"] == ch["tf"]],
key=lambda r: r["insample_sharpe"])
fn_const_anch = const_factory(tf=ch["tf"], **crow["params"])
print(f" miglior costante su {ch['tf']}: {crow['params']} IS {crow['insample_sharpe']}")
else:
fn_const_anch = fn_const
s0 = series_at_anchor(fixed_target, "fixed", hours, 0)
K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna()
print(f" sanity ancora 0 vs serie canonica del fisso: max|Δ| "
f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})")
for lab, mtr in (("Sharpe FULL", A._sh),
("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))):
b_c = A.anchor_luck_band(lambda o: series_at_anchor(fn_ch, "cand", hours, o), offs,
metric=mtr)
b_k = A.anchor_luck_band(lambda o: series_at_anchor(fn_const_anch, "const", hours, o),
offs, metric=mtr)
print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} "
f"(pctl {b_c['canonical_pctl']:.2f}) MEDIANA {b_c['median']:+.3f} "
f"banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]")
print(f" {'':<12} costante: canonica {b_k['canonical']:+.3f} "
f"(pctl {b_k['canonical_pctl']:.2f}) MEDIANA {b_k['median']:+.3f} "
f"banda [{b_k['lo']:+.3f},{b_k['hi']:+.3f}]")
dl = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o),
lambda o: series_at_anchor(fn_const_anch, "const", hours, o),
offs, metric=mtr)
print(f" {'':<12} DELTA APPAIATO (adatt - cost): mediana {dl['median_paired']:+.3f} "
f"positivo in {dl['n_positive']}/{dl['n_anchors']} "
f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}")
# -- 8-bis. ANCORA APPAIATA per la cella DAVVERO ADATTIVA ---------------------------
if fn_ad_keep is not None and best_keep["tf"] == ch["tf"]:
hr("8-bis. ANCORA APPAIATA — cella DAVVERO ADATTIVA vs miglior costante")
for lab, mtr in (("Sharpe FULL", A._sh),
("Sharpe HOLD", lambda s_: A._sh(s_[s_.index >= HOLDOUT]))):
dl = A.anchor_luck_delta(
lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o),
lambda o: series_at_anchor(fn_const_anch, "const", hours, o), offs, metric=mtr)
b = A.anchor_luck_band(lambda o: series_at_anchor(fn_ad_keep, "adatt", hours, o),
offs, metric=mtr)
print(f" {lab:<12} adattiva-vera: canonica {b['canonical']:+.3f} "
f"(pctl {b['canonical_pctl']:.2f}) MEDIANA {b['median']:+.3f} "
f"banda [{b['lo']:+.3f},{b['hi']:+.3f}]")
print(f" {'':<12} DELTA APPAIATO vs costante: mediana {dl['median_paired']:+.3f} "
f"positivo in {dl['n_positive']}/{dl['n_anchors']} "
f"banda [{dl['lo']:+.3f},{dl['hi']:+.3f}] gate_pass={dl['gate_pass']}")
elif fn_ad_keep is not None:
print(f"\n (8-bis non girata: la cella adattiva sta su {best_keep['tf']} e il costante "
f"su {ch['tf']} -> le ancore non sarebbero coppie)")
# -- 8-ter. IL CRITERIO DEL FALSIFICATORE, SU **TUTTE** LE CELLE ADATTIVE ------------
hr("8-ter. IL CRITERIO DEL FALSIFICATORE APPLICATO A **OGNI** CELLA DAVVERO ADATTIVA")
print(" Il 22/08 dichiarava: «un rilevatore il cui L_t si muove DAVVERO e batte il miglior")
print(" lookback COSTANTE in >=6/8 anni appaiati con corr->TP01 < 0.6». Non basta che")
print(" fallisca il VINCITORE: si applica il criterio a tutte e", len(ad_rows), "le celle adattive.")
print(f"\n {'tf':>4} {'cella':<58} {'corr':>6} {'anni>0':>7} {'dShFULL':>8} {'dShHOLD':>8} esito")
passed = []
for r in sorted(ad_rows, key=lambda x: -x["insample_sharpe"]):
fnr = factory(tf=r["tf"], **r["params"])
dr = A.candidate_daily(fnr, tf=r["tf"])
JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna()
cor = float(JJ2["a"].corr(JJ2["b"]))
yr = paired_by_year(dr, d_const)
nyp = sum(1 for x in yr if x["d_sh"] > 0)
Jr = pd.concat({"C": dr, "B": d_const}, axis=1, join="inner").dropna()
JrH = Jr[Jr.index >= HOLDOUT]
dF = A._sh(Jr["C"]) - A._sh(Jr["B"])
dH = A._sh(JrH["C"]) - A._sh(JrH["B"])
ok = bool(cor < 0.6 and nyp >= 6)
if ok:
passed.append((r, cor, nyp, dF, dH))
lab = (f"{r['params']['det']} {r['params']['par']} {r['params']['stat']} "
f"{r['params']['mapping']} {r['params']['blend']}")
print(f" {r['tf']:>4} {lab:<58} {cor:>6.3f} {nyp:>5}/8 {dF:>+8.3f} {dH:>+8.3f} "
f"{'PASSA' if ok else ''}")
print(f"\n celle che soddisfano ENTRAMBE le condizioni del falsificatore: "
f"{len(passed)}/{len(ad_rows)}")
cors = []
for r in ad_rows:
dr = A.candidate_daily(factory(tf=r["tf"], **r["params"]), tf=r["tf"])
JJ2 = pd.concat({"a": dr, "b": tp01}, axis=1, join="inner").dropna()
cors.append(float(JJ2["a"].corr(JJ2["b"])))
print(f" corr->TP01 sulle celle adattive: min {min(cors):.3f} mediana "
f"{np.median(cors):.3f} max {max(cors):.3f} (soglia del falsificatore 0.60)")
# -- 9. FEE SWEEP ---------------------------------------------------------------------
hr("9. FEE SWEEP — l'adattivo trada di piu': e' li' che muore o no")
for nm, f_, tfx in (("adattivo (A)", fn_ch, ch["tf"]), ("WF-PnL (C)", fn_wf, wch["tf"]),
("miglior costante (B)", fn_const, cch["tf"]),
("TP01 30/90/180", fixed_target, "1d")):
line = []
for fee in (0.0, 0.0005, 0.001, 0.0015):
line.append(f"{2*fee*100:.2f}%RT {A._sh(A.candidate_daily(f_, tf=tfx, fee_side=fee)):+.3f}")
tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a))
["turnover_per_year"] for a in ASSETS])
print(f" {nm:<22} " + " ".join(line) + f" | turnover {tt:.0f}x/anno")
hr(f"FINE — {time.time()-t0:.0f}s")
if __name__ == "__main__":
main()
+667
View File
@@ -0,0 +1,667 @@
# CRITICO DI COMPLETEZZA — ondata 2026-08-22 (`r0822b_critic`)
**Mandato:** non testare una strategia. Rispondere a *cosa non e' stato guardato, quale
affermazione e' rimasta non verificata, e qual e' la misura di maggior valore che nessuno ha fatto*.
**Materiale letto per intero:** `docs/research/BRIEF-0822.md`, `docs/research/RESULTS-0822.md`
(684 righe, 19 sezioni), `docs/diary/2026-08-22-wave-multiagente.md`, piu' i punti dei singoli
`scripts/research/r0822_*.py` dove il ledger andava verificato.
**Regola di lettura di questo file.** Distinguo tre stati e li marco:
`[VERIFICATO]` = ho aperto la fonte e il fatto e' controllato in questa sessione (comando
riportato o citazione esatta) · `[DEDOTTO]` = ragionamento su numeri altrui, non ri-misurato ·
`[DA MISURARE]` = ipotesi mia, con costo stimato. Non fabbrico numeri nuovi salvo controlli rapidi,
e quando ne faccio uno dico che e' un controllo di cinque minuti e non uno studio.
**Esito in una riga: l'ondata e' onesta nei singoli filoni e debole nella CUCITURA.** I difetti
gravi non stanno dentro i filoni (che hanno catturato 7 errori propri) ma in tre punti dove
nessuno era responsabile: (a) **due "difetti di dato" sono convenzioni dichiarate nel sorgente di
un fornitore che gira su questa stessa VPS e che nessuno ha aperto**; (b) **il gate principale del
progetto — il deflated-Sharpe — e' stato misurato lo stesso giorno ai suoi due estremi degeneri da
due agenti diversi, e nessuno ha unito le due misure**; (c) **un filone su venti (ALT-OPTIONS) non
e' nel ledger, e il suo risultato falsifica una quarta soglia pubblicata** — quella che tiene
VRP01 fuori dal libro per taglia.
---
## 1. CLAIM NON VERIFICATI
Ordinati per `(impatto x P(falso)) / costo`. Non elenco tutto: le otto che contano.
### 1.1 — 🥇 «una firm crypto lista i 19 alt Hyperliquid con short abilitato» (PROP-ALLOC)
`P(falso)` **media-alta** · `impatto` **massimo** · `costo` **un pomeriggio, zero CPU**
E' l'unica assunzione su cui poggia **l'intero risultato di testa dell'ondata**: `+0,400 di J`
(argmaxJ 25/25/50 contro il libro live 75/25) e' quasi tutto *mettere XS01 su un conto funded*, e
`+0,016` — il 4% — e' il riallocare per la barriera. Se XS01 non e' eseguibile presso la firm,
la riga argmaxJ collassa verso `75/25/0`, che nello stesso script misura **J 0,335 / P(pass) 38,6%**
invece di **0,738 / 82,2%**. Con essa cade anche il *risultato strategico* del 25/07 §4
(«sul canale funded diversificare paga perche' il vincolo binding e' il DD»), che e' il perno di
tutta la valutazione del canale prop.
⚠️ **E il progetto ha gia' una regola scritta per questo caso, e non l'ha applicata.** Il 26/07:
*«la negoziabilita' sul conto REALE va verificata quando lo sleeve entra in RICERCA, non quando
entra nel book»* — regola nata dal blocco PRIIPs su GTAA01, dove **cinque settimane di misure**
poggiavano su un'assunzione di conto mai controllata, e il controllo costo' **un ordine di prova**.
`r0822_prop_alloc.py` la ripete: alla riga 100 `CRYPTO_PROP = ("TP01","SKH01","XS01") # cio' che
una firm CRYPTO puo' davvero far eseguire` — un commento, non una misura. [VERIFICATO: letto il
sorgente; l'agente esclude esplicitamente VRP01/GTAA01 *"una firm CRYPTO non lista opzioni Deribit
ne' ETF azionari USA"*, quindi ha pensato al problema e si e' fermato un passo prima di XS01.]
**Cosa verificare, in ordine:** (a) la lista strumenti di HYROTRADER e FTMO-crypto contiene i 19
major HL (SOL/BNB/XRP/DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE oltre BTC/ETH)?
(b) short abilitato su ciascuno? (c) quale costo effettivo per gamba (spread + commissione)?
**La (c) ha una soglia gia' misurata in questa stessa ondata**: XS-LITE riporta che la famiglia XS
regge **oltre 50 bps/gamba** — quindi il costo di una firm quasi certamente non morde, ed e' la
(a)/(b) binaria che decide tutto. **Se (a) e' falsa, il verdetto LEAD del filone 1 va riscritto
oggi, non a un gate futuro.**
### 1.2 — 🥈 «`dealer_net_gamma` e' il GEX col SEGNO INVERTITO» (DEALER-GAMMA)
`P(falso)` **alta — anzi: FALSO come formulato** · `impatto` medio · `costo` **due minuti**
Il ledger lo chiama *«il risultato piu' riusabile del filone»* e scrive: *«Chi lo usasse col nome
che porta leggerebbe ogni regime al contrario»*. L'agente ha deciso la questione **per
ricostruzione** (corr −0,95 BTC / −0,89 ETH contro un GEX da manuale) e lo dichiara esplicitamente
nel proprio codice: `"Quale, lo decide la sezione 2 (ricostruzione dalla catena), non io"`
(`r0822_dealer_gamma.py:187`).
🚨 **La fonte autorevole e' su questa VPS e dice il contrario.** [VERIFICATO] La colonna non e'
calcolata da cerbero-bite: bite **inoltra e basta** il campo `total_net_dealer_gamma` che riceve
dal tool MCP `get_dealer_gamma_profile` (`src/cerbero_bite/runtime/market_snapshot_cycle.py:85` e
`clients/deribit.py:342-384`, letti dal repo Gitea `Adriano/Cerbero-Bite`). E il produttore e'
`/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py`, **acceso adesso**, che in testa al
file dichiara la convenzione:
```
# Convention dealer gamma: i dealer sono SHORT calls (le vendono al retail) e
# LONG puts. ... Net dealer gamma negativo -> mercato instabile (squeeze ...)
...
entry["call_dealer_gamma"] -= contrib # dealer short calls
entry["put_dealer_gamma"] += contrib # dealer long puts
```
Cioe': **call negative, put positive** — l'esatta negazione, su entrambe le gambe, della
convenzione da manuale che l'agente ha usato (`call +, put −`, `r0822_dealer_gamma.py:237`).
**Non e' un difetto della colonna: e' una convenzione opposta, deliberata e documentata** — e nel
suo frame `dng < 0` significa *dealer short gamma / mercato instabile*, che e' proprio la
semantica che il docstring di bite dichiara e che l'entry filter §2.8 usava.
E c'e' una **seconda spiegazione, gratis, per i due numeri residui che l'agente ha attribuito a
non-portabilita'**: la chiamata di bite usa il default `top_n_strikes = 50` [VERIFICATO:
`dealer_gamma_profile(asset_upper)` senza argomenti], contro i **~280-316 strike** della
ricostruzione. Un troncamento ai 50 strike di punta spiega naturalmente sia il coefficiente
`−0,7` (invece di `−1,0`) sia l'offset *«dng=0 corrisponde a GEX=+5,8e6 su BTC»*, che il ledger
presenta come prova che la soglia non e' portabile.
**Cosa cambia se e' falso (ed e' falso):** il verdetto **SCARTATO regge intatto** (DSR 0,440,
null de-levering fallito, `n_eff` 24-53 ore su 2.130, cella migliore *sotto* il massimo del
rumore). Ma cade la **regola pubblicata**, e cade nel modo peggiore: la frase del ledger *«BTC non
separa, ETH separa al ROVESCIO»* e' formulata in un frame di segno che va rovesciato — quindi
l'unica riga del filone che le ondate future erediterebbero e' quella sbagliata. **Correzione da
fare oggi nel ledger, costo due righe.**
### 1.3 — 🥉 «`liquidation_long/short_risk` e' costante 'low': meta' dell'ipotesi non esiste, ed e'
comunque morta perche' la colonna non e' ricostruibile» (FLOW-SQUEEZE)
`P(falso)` **alta sulla seconda meta'** · `impatto` medio · `costo` **due ore**
[VERIFICATO] Il fatto e' vero (`low` in 17.229/17.229 righe non nulle, riprodotto). Ma le due
inferenze che l'agente costruisce sopra il fatto sono entrambe smentibili leggendo la stessa
sorgente di §1.2, `/opt/docker/cerbero-mcp/.../sentiment/fetchers.py:480-518`:
```
long_risk = "high" se avg_funding > 0.0001 e delta_24h > 5
"medium" se avg_funding > 0.00005 e delta_24h > 2
```
Cioe' **e' una soglia su due input, non una misura**. Conseguenze:
- *«Nessuna fonte nostra la produce, e non c'e' niente da produrre»* (`r0822_flow_squeeze.py:626`)
e' **falso**: i due input sono `oi_delta_pct_24h` e il funding cross, e **il funding cross e' nel
file** (`funding_cross_annualized`, copertura 99%), cosi' come `oi_delta_pct_4h` (copertura 99%,
dev.std 1,23%, range −7,1%…+13,4% — [VERIFICATO] con una lettura del parquet). L'agente poteva
ricostruire la variabile di affollamento **con le proprie soglie**, che e' anche metodologicamente
meglio che ereditarne una arbitraria.
- *«Costante = zero informazione»* e' vero come statistica e **sbagliato come lettura**: `low` al
100% con quelle soglie significa che **nella finestra 2026-03 → 07 non c'e' stato un solo
episodio di affollamento** per quella definizione. Non e' una colonna rotta, e' un fatto di
mercato — e per giunta **rafforza** la conclusione del filone (funding chiuso sul quarto lato)
molto piu' di quanto faccia «la colonna e' inservibile».
**Cosa cambia se e' falso:** il verdetto SCARTATO regge (l'altra meta' e' sotto la propria MDE
dichiarata: effetto 0,402% contro MDE 1,056%). Ma il filone e' chiuso con la motivazione
sbagliata, e la motivazione sbagliata e' quella che impedisce di riaprirlo con i dati che ci sono.
### 1.4 — «la finestra forward non va persa: riparare `advance()` + RIGENERARE, nessuna data di
gate si sposta» (MONITOR-AUDIT)
`P(falso)` bassa sul fatto, **alta sull'implicazione** · `impatto` **alto (3 gate)** · `costo` zero
Il fatto e' provato bene, due volte in modo indipendente (`paper_prevday` 1427/1488 barre identiche
al bit dopo 62 notti; le 6 barre di `paper_statarb` recuperate dopo il guasto EPERM coincidono al
bit). **Non contesto il fatto. Contesto che sia gratis.**
Una serie **rigenerata** non e' una serie **forward**. La ragione per cui un progetto
pre-registra una finestra forward e' che sia prodotta da codice che *non poteva* essere tarato su
di essa. Dopo la riparazione, la serie su cui si decide il 27/09 sara' prodotta da codice
**scritto oggi, dopo aver visto quella finestra** — e chi scrive la riparazione ha gia' letto,
nel ledger, che la metrica registrata dice +1,96 e quella ricostruita −2,02. La riparazione e'
banale e meccanicamente forzata (scartare la barra non chiusa), quindi il rischio e' piccolo; ma
**e' un grado di liberta' nuovo su un gate pre-registrato, e nessuno lo ha nominato.**
**Costo di chiuderlo: zero.** Si scrive la riparazione, la si congela con un test, e **solo
dopo** si rigenera — dichiarando in anticipo che nessun parametro verra' toccato in base al
risultato della rigenerazione. Farlo dopo aver visto lo Sharpe rigenerato non e' piu' possibile.
### 1.5 — «haircut a $5.000 <= 40%» come gamba del gate XSR01 del 23/10
`P(che il gate sia non-informativo)` **alta** · `impatto` **alto** · `costo` **zero (e' gia' misurato)**
Due filoni hanno colpito le due gambe del gate del 23/10 **senza che nessuno tirasse la somma**:
- **gamba haircut** — HL-EXEC: a $5.000 col pavimento vero l'haircut sta fra **−7% e +2%**, quindi
*«una condizione pre-registrata che non puo' fallire non e' una condizione»*. XSR-REPRO
dall'altro lato: il **$14,41** pubblicato **non ha alcuno script committato che lo riproduca** ed
e' **~2,4x ottimista** (ticket mediano $3,33, 63% degli ordini sotto $5).
- **gamba Sharpe >= 1,0** — MONITOR-AUDIT: la serie che la misura registra **41 minuti di mercato
al giorno**, `corr` col replay **−0,045**.
📌 **La somma, che il ledger non fa: al 22/08 il gate del 23/10 e' non-informativo su ENTRAMBE le
gambe** — una non puo' fallire, l'altra e' letta su uno strumento scollegato dalla realta'. Il
ledger scrive «GATE 23/10 COMPROMESSO: PARZIALMENTE». **E' compromesso interamente**, e la
differenza non e' semantica: «parzialmente» autorizza a decidere il 23/10 con una riserva,
«interamente» obbliga a **ri-registrare il gate prima**, che e' l'unica mossa che non e' selezione.
### 1.6 — «k* empirico ~10-14x» (GROWTH-POLICY)
`P(falso)` **alta come numero** · `impatto` medio-alto · `costo` zero (giunzione di due misure gia' fatte)
L'agente ha gia' fatto due autocritiche corrette (capitale mediano $4,4e12 a k=14x → *«a k>2 i
numeri sono aritmetica, non previsioni»*; k* lineare nell'errore del drift). Ne manca una terza, e
sta **dentro la stessa ondata**: SLIP-AUDIT misura che la partecipazione al nastro scala lineare
col nozionale e che il caso peggiore osservato e' gia' **21,9% di una barra 5m** a nozionale ~$636.
La leva moltiplica il nozionale esattamente come il capitale. Quindi [DEDOTTO, aritmetica diretta
sulla tabella di SLIP-AUDIT]:
| leva su $635 | nozionale | peggior partecipazione osservata, riscalata |
|---|---|---|
| 1,00x | $636 | 22% di una barra 5m |
| **1,50x** (il gradino proposto) | $954 | **33%** |
| 5x | $3.180 | 110% |
| **10-14x** (k*) | $6.400-8.900 | **220-310%** |
📌 **Il gradino 1,00 → 1,50x NON e' toccato da questo** (33% resta dentro il regime misurato):
la raccomandazione operativa sopravvive. **E' il numero k\* che non e' misurabile**, per una terza
ragione indipendente dalle due dichiarate: sopra ~k=5 la serie di ritorni su cui k\* e' calcolato
smette di essere indipendente dalla size, che e' esattamente l'ipotesi che l'agente aveva segnalato
come implausibile senza poterla quantificare. **Ora e' quantificata, gratis, e va scritta accanto
al 10-14x.**
### 1.7 — «BTC opzioni min 0.1 = $6.210/lotto → VRP01 fuori a $3.000, servirebbe il 61% del conto»
`P(falso)` **verificata FALSA** · `impatto` **alto** · `costo` gia' pagato — il numero esiste e non e' nel ledger
Questo e' congelato in `tests/test_vrp_profit_take.py` e in CLAUDE.md. VRP-QUOTE-VERE l'ha
falsificato a meta' (famiglia sbagliata) e **ALT-OPTIONS l'ha falsificato del tutto** — ma
ALT-OPTIONS **non ha una sezione nel ledger** (vedi §5.1), quindi il numero non e' mai stato
scritto. [VERIFICATO: letto l'output del filone]:
| famiglia | f_venue | credito eseg. | **max-loss / lotto** | IM se il venue NON netta |
|---|---|---|---|---|
| **ETH_USDC** | **0,980** | $3,38 | **$16,62** | $33,50 |
| BTC_USDC | 0,921 | $6,25 | $33,75 | $101,79 |
| ETH inverse (la famiglia del muro) | 0,930 | $29,36 | $145,88 | $324,42 |
| BTC inverse (la famiglia del muro) | 0,927 | $81,97 | $418,03 | $1.017,06 |
Il muro pubblicato e' calcolato sul **nozionale del lotto minimo della famiglia inverse**. La
grandezza giusta per una struttura a rischio definito e' il **max-loss**, e sulla famiglia che il
conto puo' marginare vale **$16,62**. Al peso di libro del 12%, VRP01 entra con un lotto da un
conto di **~$140** (o ~$280 se Deribit non netta le gambe), non da $3.000 al 61% del conto —
**due ordini di grandezza**. Sul conto vero ($635) ci stanno **7 strutture dentro un budget di
rischio del 20%**, con **3.000 lotti al miglior bid** e spread della gamba corta al **2,6%**.
⚠️ **Cosa NON cambia, e va detto forte:** VRP01 resta fuori dal libro, ma **per la regola
«niente short-vol da modello in deploy» e per il gate IV-rank (0/19 settimane aperte)**, non per
la taglia. Cade *una* delle tre ragioni, ed e' quella che il progetto ripete piu' spesso.
### 1.8 — «lo screen largo non puo' passare il proprio gate: e' aritmetica» (ORTHO-SCREEN)
`P(falso)` **alta come generalizzazione** · `impatto` **alto (tocca 3 gate futuri)** · `costo` **5 minuti**
Vedi §2.1: l'ho ri-derivato e il numero e' giusto ma **non dice cio' che il ledger gli fa dire**.
---
## 2. CONTRADDIZIONI FRA AGENTI
### 2.1 — 🚨 IL DEFLATED-SHARPE MISURATO AI SUOI DUE ESTREMI DEGENERI, LO STESSO GIORNO, DA DUE AGENTI CHE NON SI CITANO
- **ORTHO-SCREEN**: *«su 168 trial il massimo atteso dal puro rumore e' Sharpe 1,572, sopra il
soffitto direzionale (~1,3) → una ricerca a rete larga NON PUO' passare il proprio gate. Non e'
sfortuna, e' aritmetica.»* DSR di screen **0,034**.
- **SCETTICO su VOL-SIZE**: *«`DSR = 1,000` dichiarato **VACUO, non PASS**: lo passa anche il
baseline — su una famiglia di perturbazioni della stessa strategia il deflated-Sharpe non ha
potenza.»*
**Sono la stessa osservazione dai due lati, e insieme delimitano il gate principale del progetto.**
[VERIFICATO] Ho aperto `altlib.deflated_sharpe`: il massimo atteso non e' `SE(Sharpe) x sqrt(2 ln N)`
ma la formula Bailey–Lopez de Prado
```
sr0 = sd(Sharpe dei trial OSSERVATI) x [ (1-gamma)*z(1-1/N) + gamma*z(1-1/(N e)) ]
```
cioe' **la dispersione trasversale delle celle che hai deciso di dichiarare**, moltiplicata per un
fattore che dipende solo da N. Ricalcolando il moltiplicatore [controllo di cinque minuti]:
| N dichiarato | moltiplicatore | `sr0` con la STESSA dispersione (sd = 0,58) |
|---|---|---|
| 12 | 1,665 | 0,97 |
| **24** | 1,980 | **1,15** |
| 48 | 2,261 | 1,31 |
| 72 | 2,413 | 1,40 |
| **168** (lo screen) | 2,708 | **1,57** |
| 729 | 3,164 | 1,84 |
E al variare della dispersione, a parita' di dati:
| dispersione delle celle | `sr0(24)` | `sr0(168)` |
|---|---|---|
| 0,15 (famiglia di perturbazioni, tipo VOL-SIZE) | 0,30 | 0,41 |
| 0,58 (lo screen ortogonale) | 1,15 | 1,57 |
📌 **Tre letture che il ledger non ha:**
1. **La soglia «1,572» non e' una proprieta' di BTC/ETH: e' una proprieta' della GRIGLIA.** Le
stesse 168 celle, dichiarate come 7 famiglie da 24, danno `sr0 = 1,15` — **sotto** il soffitto
direzionale. Il verdetto «impossibile per aritmetica» si ribalta **senza toccare un dato**.
2. **Percio' la lezione pubblicata invita alla mossa che il progetto ha gia' proibito.** Il ledger
conclude *«le ondate future o dichiarano famiglie molto piu' piccole in anticipo, o cambiano
meccanismo»*. La prima meta' e' **esattamente** il barare che il 30/07 aveva codificato:
*«il deflated-Sharpe e' l'unico posto dove barare e' indolore e invisibile»*, e
*«il verdetto si ribalta col conteggio»* (congelato in un test su VRP01: DSR 0,983 a N=8 /
0,948 a N=72 / 0,909 a N=360). **Il 22/08 la stessa leva viene raccomandata come buona pratica.**
3. **Cio' che il progetto non sapeva e' la META' PIU' GRANDE: la dispersione.** Il fattore N va da
1,67 a 3,16 su due ordini di grandezza di trial (1,9x). La dispersione va da 0,15 a 0,58 fra una
famiglia omogenea e uno screen (3,9x). **Il gate e' dominato dalla variabile mai dichiarata.**
**Conseguenza operativa immediata**, perche' tocca tre gate pre-registrati: DVOLSPREAD 24/10 ha
come criterio (c) *«deflated-Sharpe ricalcolato >= 0,95»* — su una famiglia di 729 celle
omogenee; XSR01 23/10 poggia su DSR 0,983; VOL-SIZE ha gia' dichiarato il proprio DSR vacuo.
**Nessuno dei tre e' interpretabile senza dichiarare, accanto a N, la dispersione delle celle.**
Riformulazione onesta della lezione di ORTHO-SCREEN: *con celle disperse di ~0,58 di Sharpe e un
soffitto di ~1,3, il gate non separa; la cura non e' dichiarare meno trial, e' progettare famiglie
i cui membri siano varianti di UN meccanismo — e li' il DSR e' vacuo, quindi serve un altro gate.*
**Il deflated-Sharpe non ha una regione utile in mezzo, e nessuno l'ha detto.**
### 2.2 — Lo Sharpe di XSR01: non tre lenti ma **quattro**, e la quarta non e' nel quadro conciliato
Il ledger conclude con un «quadro conciliato»: titolo `demean_skeptic` **1,82** · gate
`basket_gate` **1,75-1,79** · monitor `paper_xsr` **2,21**, e prescrive
*«NUMERO ONESTO DA CITARE: 1,79 (finestra di scoperta) / 1,63 (a oggi)»*.
[VERIFICATO nell'output di XS-LITE] Lo stesso giorno **XS-LITE**, che dichiara replica **bit-exact**
contro `basket_from_positions(demean=True)` (`max|diff| = 0,000e+00` su 965 barre), pubblica:
*«XSR01 pieno, lente pubblicata: Sharpe **1,64** (pubblicati 1,82)»*, *«normalizzazione COSTANTE
1/50: **1,65**»*, *«sulla FINESTRA DI SCOPERTA (<= 2026-07-25, 937 barre): **1,75**»*.
Quindi **due agenti che rivendicano entrambi la replica bit-exact della stessa funzione danno 1,75
e 1,79 sulla stessa finestra.** La spiegazione piu' probabile e' la loro stessa scoperta (l'ultima
barra della finestra di scoperta era parziale il 25/07 e oggi e' chiusa), e i due concordano
sull'oggi (1,64 vs 1,63). **Ma il ledger prescrive una coppia canonica senza nominare l'altra**, e
la regola che pubblica — *«va sempre citata la coppia (lente, ultima barra chiusa)»* — e' proprio
quella che avrebbe risolto la discrepanza se applicata a se stessa. **Costo di chiudere: una riga
nel ledger. Impatto: e' il numero su cui si decide il 23/10.**
### 2.3 — Il muro di XS01: **tre numeri diversi** nello stesso corpus di documenti
- XS-LITE: capitale minimo perche' il ribilancio passi = **$109 di sleeve (~$730 di libro)**.
- HL-EXEC: haircut ~0 gia' a **$200-600 allocati** (→ a peso 15%, **$1.300-4.000 di conto**).
- Diario §6.5: *«XS01 e' eseguibile a **~$1.300 di conto**»* — cioe' l'estremo basso di HL-EXEC.
I tre sono compatibili (criteri diversi: soglia del ribilancio vs soglia dell'haircut), ma il
diario ne pubblica **uno** come se fosse *il* numero, dopo che l'ondata ha appena passato una
giornata a dimostrare che i numeri pubblicati a un criterio solo sono il difetto ricorrente del
progetto. **Da citare come banda `$730-$1.300` col criterio accanto.**
### 2.4 — La leva: due filoni della stessa ondata danno guida opposta
- **GROWTH-POLICY**: k\* ~10-14x, il libro gira al 7% di Kelly, **alzare** la leva vale 14,7a → 11,6a.
- **PROP-ALLOC**: J massimo a **0,75x**, e *«P(pass) senza limite di tempo e' monotona
DECRESCENTE»* (98,9% a 0,50x → 65,9% a 1,50x).
Non e' un errore: sono obiettivi diversi (crescita in log senza barriera vs sopravvivenza sotto
una barriera per-conto). **Ma nessuno lo scrive**, e l'operatore leggera' due raccomandazioni
opposte nello stesso documento. La sintesi che manca, e che riconcilia:
📌 **anche il conto proprio ha una barriera assorbente** — e' la decisione di venue del 26/07
(*«zero e' assorbente: andare a zero all'anno 10 di un piano da 16 anni significa non arrivarci
piu'»*). Quindi il criterio di GROWTH-POLICY (Kelly puro) e' il caso limite senza barriera, e
PROP-ALLOC misura cosa succede quando la barriera c'e': **la leva ottima cade da 10x a 0,75x
appena si mette una barriera, e la distanza fra i due numeri E' la misura di quanto la barriera
costa.** Detta cosi', le due misure smettono di contraddirsi e diventano la stessa curva.
### 2.5 — Il peso di SKH01: respinto da un filone, raccomandato al doppio dall'altro
- **VOL-SIZE / scettico**: *«quasi ogni variante alzava il peso effettivo di SKH fino a 0,375, e
"alzare SKH01" e' gia' stato misurato e respinto il 26/07»* — motivo per cui il null giusto e'
iso-peso.
- **PROP-ALLOC**: sul campione 2019+ che contiene il 2022, l'ottimo della coppia crypto sotto
barriera e' **38/62** (J 0,521 contro 0,443), cioe' **SKH01 al 62%**.
Di nuovo obiettivi diversi, di nuovo non detto. Ma qui c'e' un aggravante che **l'agente dichiara
da solo**: PROP-ALLOC non ha girato la banda d'ancora, quindi il suo ottimo poggia sull'ancora
canonica di **SKH01, lo sleeve che il LOO del 26/07 misura come quello con la fortuna d'ancora
piu' grande da restituire** (l'ancora regala 2/3 del FULL, 70% dell'hold-out, 80% della protezione
DD). **La distorsione e' a favore della raccomandazione, e la raccomandazione riguarda l'unico
canale che l'operatore potrebbe aprire quest'anno.** Costo di chiuderla: rigirare l'ottimo su una
banda di 8 ancore — l'ordine di grandezza del budget di un agente di questa ondata.
### 2.6 — Il conteggio degli ingressi VRP nella stessa finestra di catena: 19 contro 22
VRP-QUOTE-VERE: *«f = 0,714 pooled, **0/19** osservazioni >= 1,0»*; SKEW: *«0,714 su **22**
ingressi»*. Stesso `f` a tre decimali, popolazione diversa. [VERIFICATO che SKEW usa
*«stessa macchina di `cblib`»*, quindi **non e' una replica indipendente** e l'accordo a tre
decimali non e' evidenza.] Spiegazione benigna probabile (VRP richiede **entrambe** le gambe
quotate ai due delta obiettivo, SKEW ricostruisce lo smile e ne richiede meno). **Ma il ledger
presenta il 0,714 come «replica indipendente» del f del 30/07 senza dire che il secondo 0,714
viene dallo stesso harness.** Impatto basso, costo di chiarirlo nullo.
### 2.7 — «il collettore raccoglie la famiglia sbagliata» e' una classificazione, non un fatto
Il ledger apre VRP-QUOTE-VERE con 🚨 **IL RISULTATO DI PRODUZIONE**. [VERIFICATO in
`scripts/live/collect_chain.py:114`: `{"currency": asset, "kind": "option"}` e `OI_MIN = 100.0`
alla riga 57 — i due fatti sono esatti.] Ma il collettore serve la **ricerca sui prezzi**, non
l'esecuzione: sulle inverse ci sono 415/548 strumenti con OI>=100 e **97% di quote a due lati**,
contro il 94% e 119 strumenti di ETH_USDC. Per misurare `f`, IV-rank e struttura a termine, la
famiglia raccolta e' **la migliore delle due**. Il difetto esiste solo rispetto a un uso —
l'esecuzione — che oggi e' vietato da un'altra regola («niente short-vol da modello in deploy»).
Il diario lo corregge a meta' (punto aperto 3 lo presenta come decisione sul rate limit); il
ledger no. **Un difetto di produzione e una scelta di raccolta non si segnano con lo stesso
simbolo**, o il prossimo che legge ripara qualcosa che non e' rotto — al costo di **+90% sul giro**
e del rate-limit per-IP che il 29/07 e' gia' costato un guasto.
---
## 3. MODALITA' NON ESPLORATE
Filtro applicato: (a) compatibile con dati che il progetto possiede o puo' ottenere, (b) non nella
lista dei filoni morti del brief, (c) meccanismo plausibile e nominabile. Niente idee generiche.
### 3.1 — 🥇 Il costo del CHOP, cioe' il rischio che il libro non ha mai vissuto
**Dati:** esistono (7,4 anni di libro; 2018-19 e 2022-23 come stiramenti laterali; 30 anni di
equity come stampo). **Meccanismo:** il progetto ha misurato la difesa nel **sinistro** — il
criterio B di `edge_watch` e' 8/8 anni di crash superati — e non ha **mai** misurato la modalita'
di fallimento propria di un trend-follower, che non e' il crash ma il **mercato laterale**. E la
misura di controllo che ho fatto in cinque minuti la rende urgente e non teorica:
| giorno | BTC | ETH | **libro 75/25 (close-only)** |
|---|---|---|---|
| 2020-03-12 | **−41,5%** | **−45,7%** | **+0,97%** |
| 2022-06-13 | −15,5% | — | +0,72% |
| 2021-05-19 | −14,3% | −27,9% | −1,98% |
| 2022-11-09 | −14,4% | −18,0% | 0,00% |
| 2021-01-21 | — | −19,5% | −2,24% |
[controllo di cinque minuti, non uno studio: `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`,
75/25, ancora canonica. Peggior giorno del libro su 7,4 anni: **−3,38% (2025-10-10)**, che **non e'
un giorno di crash**.] Cioe': **il libro non perde nei crolli — nei crolli guadagna.** Tutte le sue
peggiori giornate sono altrove. **Nessuno ha mai chiesto dove.** Un piano a 10-20 anni su una
strategia il cui unico rischio misurato e' quello che non la tocca ha un buco esattamente della
forma che questo progetto e' bravo a trovare in tutto il resto.
### 3.2 — 🥈 Il prezzo di essere ciechi: il costo atteso in P&L dell'attrito OPERATIVO
**Dati:** ci sono gia' e sono nostri — `logs/cron_book.log`, `data/venue_watch`, `feed_age_minutes`,
`skh_feed_errors`, `book_executions.jsonl`. **Meccanismo:** nel 2026 il libro e' stato cieco o
stantio almeno tre volte documentate (feed-freeze 14/07; fallback silenzioso di `fresh_5m` il
29/07, latenza d'uscita di SKH01 da ~1h a **~11h** per 6 giri su 8; manutenzione Deribit del 18/08
sforata **fino alle 14:07**, con astensione del libro). Il progetto ha prezzato fee, slippage,
min-order, pavimento IB, fortuna d'ancora, degrado d'esecuzione, e persino **il fallimento
dell'exchange** — e non ha **mai** prezzato *quanto costa all'anno che il libro non veda*. E' una
misura, non una previsione: si prendono le finestre di cecita' realmente occorse, si guarda cosa ha
fatto il prezzo dentro, e si confronta il P&L realizzato col P&L del percorso non-cieco.
**Perche' conta adesso:** e' un costo che **scala col nozionale** esattamente come la leva, quindi
entra direttamente nel conto di §4; e le uscite di SKH01 sono la gamba latency-sensitive.
**Perche' non e' morto:** non e' calendario, non e' un overlay, non e' una strategia — e' il
prezzo di un rischio operativo gia' materializzato tre volte.
### 3.3 — 🥉 Lo spread di volatilita' BTC-vs-ETH come sleeve, non come timer
**Dati:** `dvol_btc` / `dvol_eth` dal 2021-03 = **1978 giorni, 5,4 anni** — l'unico dataset non
direzionale del progetto abbastanza lungo da avere un hold-out vero (a differenza di tutta la
catena, ferma a 74-90 giorni). **Meccanismo:** i due asset condividono un fattore di vol comune, e
la *ricchezza relativa* delle due vol e' una grandezza stazionaria per costruzione, mentre il
livello non lo e'. **Non e' DVOLSPREAD:** quello e' il lead in forward-monitor che usa lo spread
come **timer direzionale** (de-risk); qui lo spread si traderebbe **come tale** (straddle lungo su
uno, corto sull'altro), market-neutral rispetto alla direzione e — a differenza di VRP01 — con
esposizione al premio di vol quasi cancellata fra le due gambe. **Non e' nella lista dei morti**
(gamma scalping = long-vol nudo; VRP01 = short-vol nudo gated; questo e' relative-value).
⚠️ **Due muri dichiarati in anticipo:** (i) sarebbe prezzato su DVOL, cioe' BS-flat, e la SKEW di
questa ondata ha appena misurato che il flat costa **x0,869 di struttura a termine** e **x0,920 di
skew** — quindi il verdetto massimo e' LEAD finche' non e' riprezzato sulla catena vera; (ii)
richiede opzioni su entrambi gli asset: fuori a $635 (BTC_USDC ~$780/lotto), dentro a ~$3-5k.
### 3.4 — Il basis dei futures datati come **campione della coda**, non come carry
BASIS-CALENDAR ha costruito un dataset che il progetto non aveva — **BTC 34 contratti / 240.150
barre orarie dal 2018-09, ETH 34 / 233.334, piu' 64.110 ore di funding e indice** — e lo ha usato
per una sola domanda (il premio incassabile, IC95 che contiene lo zero → SCARTATO). **Il dataset
contiene la coda che manca a tutto il resto del progetto**, e il ledger lo dice esplicitamente
(*«include il deleveraging 2022 — esattamente la coda che a CC01 mancava per costruzione»*).
**Uso non esplorato:** il blow-out del basis e' l'unico marcatore osservabile e datato dei giorni
di deleveraging forzato. Condizionare a quei giorni le domande di §3.1 e §4 da' un **campione di
stress reale** al posto di uno scenario assunto a mano.
⚠️ **E c'e' un'azione a costo quasi nullo che nessuno ha messo negli aperti:** quel dataset vive in
`.../scratchpad/basis_cache/` (**20 MB**, [VERIFICATO]), cioe' in una cartella di sessione che
sparisce. E' ricostruibile (30/30 trimestrali rispondono e il metodo — costruire a mano i nomi dei
contratti scaduti e chiamare `get_tradingview_chart_data` — e' documentato nello script), ma
ricostruirlo costa rete e tempo che ora sono gia' stati spesi. **Persisterlo in `data/raw/fut_*`
e' l'unica cosa dell'intera ondata che si perde per non averla scritta.**
### 3.5 — La copertura di coda come **abilitante della leva**, non come miglioramento dello Sharpe
**Stato dell'arte nel progetto, dichiarato:** tail hedge *e' gia' stato provato* — `OPT06` (ratio
put spread con tail hedge) e `OPT07` (collar) nello sweep del 20/06, entrambi **modellati su DVOL
BS-flat** e giudicati **sullo Sharpe**, entrambi bocciati. **Cosa non e' stato fatto:** giudicarlo
sull'obiettivo **crescita a leva**. Sono domande diverse perche' il valore di un pavimento e'
**convesso nella leva**: a k=1 una put lontana e' un costo secco, a k=k\* e' cio' che rende k\*
stimabile. La domanda esatta: *esiste una put a delta basso il cui costo annuo, misurato sulle
quote VERE (non su DVOL), sia minore del guadagno di crescita che il pavimento autorizza?*
⚠️ **Prior onesto: sfavorevole.** Il 30/07 ha misurato che l'ala comprata costa **~2,3x il
modello** (f_long 2,23, fino a 5,85 sui delta piu' bassi) — quindi la protezione reale e' piu' cara
di quella che aveva gia' bocciato OPT06/OPT07. **Ma il confronto non e' mai stato fatto contro la
leva**, ed e' l'unico confronto in cui una protezione cara puo' comunque pagare.
⚠️ Eseguibile solo da ~$3-5k (ETH_USDC ~$250/lotto di nozionale): **e' una domanda per il deposito,
non per oggi.**
### 3.6 — Il flusso di opzioni (volume per strike), che e' nel dato e nessuno ha guardato
La catena registra `volume_24h` **per strumento**, oraria. Sei filoni hanno usato l'**OI**
(posizioni accumulate) e **zero** hanno usato il **volume** (transazioni). ALT-OPTIONS ha appena
dimostrato che sulle famiglie lineari OI e negoziabilita' sono **anti-correlati (rho di rango
−0,77)** — che e' un'ottima ragione per sospettare che le due grandezze dicano cose diverse anche
sulle inverse. **Meccanismo:** un'ondata di acquisti concentrata su uno strike e' un fatto di
flusso; l'OI e' il suo integrale, e integrare distrugge il segnale di timing.
⚠️ **Muro invalicabile e dichiarato: 74-90 giorni di calendario.** Il verdetto massimo qui e'
**LEAD con gate pre-registrato**, e l'onesta' vuole che si dica prima di iniziare, non dopo.
La ragione per farlo comunque e' che **il calendario cresce da solo**, gratis, ogni ora.
---
## 4. LA MISURA DI MAGGIOR VALORE MAI FATTA
### La misura
**Il peggior giorno che il libro puo' avere, misurato invece che assunto: la distribuzione della
perdita giornaliera CONDIZIONATA all'esposizione piena, sotto la lente wick accoppiata, con lo
scenario di gap attraverso il disaster-SL.**
### Perche' questa
Perche' e' **l'unico parametro che blocca la sola leva gratuita che l'ondata abbia trovato**, e
perche' oggi quel parametro e' **un numero scelto a mano**.
GROWTH-POLICY ha stabilito che il gradino eseguibile `1,00x → 1,25-1,50x` vale
**14,7 anni → 12,9-11,6 anni** al capitale-rendita (a €500/mese, netto fisco). Lo scettico ha
tolto una delle tre riserve (il wick non amplifica il maxDD: ricarico costante ~3,5%). Delle due
rimaste, **una non e' risolvibile** (il drift stimato su 7,4 anni — e comunque k\* resta 7x a −2 SE,
quindi 1,5x sta al 21% di k\*) e **una lo e': la coda.** E' la riserva che decide, perche' e' l'unica
che porta k\* **sotto** il gradino proposto: *«un solo giorno −10% all'anno porta k\* da ~10x a 2x,
−15%/anno a 1x»*.
📌 **Quel −10% non e' stato misurato: e' stato scelto.** E il controllo di cinque minuti in §3.1
dice che potrebbe essere **una coda da buy&hold importata dentro un libro difensivo**: sui cinque
peggiori giorni di BTC e i cinque di ETH in 7,4 anni — compreso il **−41,5% / −45,7% del 12 marzo
2020** — il libro ha fatto **+0,97 · +0,72 · 0,00 · −1,98 · −2,24 · +0,23%**, e il suo peggior
giorno assoluto e' **−3,38%** in una data che non e' un crash. Se il vero peggior giorno
plausibile e' −5% e non −10%, k\* non e' 2x e **il gradino e' autorizzato**.
⚠️ **Ma il mio controllo e' close-only, cioe' proprio la lente che questa ondata ha appena
dichiarato "esattamente cieca" sulle regole a UN giorno** (rapporto acc./close = INF a soglia 5%).
Quindi **la misura vera non e' quella che ho fatto**: e' la stessa domanda sotto
`r0725_prop_coupled` (tuple accoppiate, minimo intra-giorno) piu' lo scenario che il dataset non
contiene — un gap notturno che attraversa il disaster-SL −30% con posizione piena su entrambe le
gambe. Tutta la macchineria esiste gia' (`r0725_prop_coupled`, `r0822_leverage_skeptic`), il
campione di stress si puo' prendere dal dataset dei futures datati di §3.4, e il costo e'
**un agente di questa ondata**.
### Confronto esplicito con le leve gia' quantificate
| leva | valore | costo | stato |
|---|---|---|---|
| versare €250/mese invece di €0 | da **mai** a 19,8a (P 52% netto fisco) | €250/mese | decisa dall'operatore |
| versare €500 invece di €250 | 19,8a → **14,7a** | €250/mese in piu' | decisa dall'operatore |
| **leva 1,00x → 1,50x** | **14,7a → 11,6a** | **zero** | **bloccata da UN parametro assunto** |
| riallocare per la barriera (prop) | +0,016 di J = **il 4%** | analisi | misurata, marginale |
| XS01 su conto funded | +0,400 di J | **assunzione non verificata (§1.1)** | LEAD |
| ottimizzare il peso di SKH01 | **+0,030 di Sharpe** | analisi | gate fallito, plateau |
| rischio venue a p=5% | capitale mediano → **zero** | split di conto | decisa: 100% Deribit fino a $20k |
📌 **Il gradino di leva vale ~3,1 anni, cioe' l'equivalente di ~€300/mese di versamenti, a costo
zero.** E' la prima volta in due mesi che una misura di ricerca entra nell'ordine di grandezza
della leva dei versamenti, e ci entra perche' **non e' ricerca di alpha: e' un cambio di scala di
cio' che gia' funziona.** Vale **100x** l'ottimizzazione del peso di SKH01 e **25x** il riallocare
per la barriera.
### E adesso la parte che il mandato chiede di dire se e' vera
**Sì: nessuna misura di ricerca di questa ondata batte "versare".** Venti filoni, ~15.300 righe,
**zero candidati promossi**; il soffitto direzionale e' stato riconfermato per via aritmetica; e
per **tre volte** l'eseguibilita' a $600 non e' stata il vincolo binding — tutto e' morto
sull'**edge**. La misura che propongo **non e' un'eccezione a questo, ne e' la conferma**: non
cerca un rendimento nuovo, **prezza un parametro di rischio per poter usare meglio quello che c'e'**.
E anche cosi', 3,1 anni sono **meno** di cio' che valgono €250/mese di differenza nei versamenti.
**La seconda misura, e va detta insieme alla prima perche' costa ancora meno:** la verifica di
§1.1 (i 19 alt HL sono listati e shortabili presso la firm?). Non e' ricerca, e' **un pomeriggio di
lettura sul sito di due firm** — e decide se il canale prop, l'unico che moltiplica il nozionale
senza possedere capitale, vale J 0,738 o J 0,335. **Il progetto ha gia' pagato una volta il prezzo
di non fare questa verifica** (GTAA01/PRIIPs, cinque settimane di misure su un ETF che il broker
rifiuta di vendere) e ha scritto la regola per non ripeterlo. **Farla prima di aprire un altro
filone prop e' la cosa piu' economica che ci sia in questo elenco.**
**In una riga:** *misurare la coda invece di assumerla* e' la ricerca che vale di piu' (3,1 anni,
gratis); *leggere il listino di una prop firm* e' la verifica che vale di piu' (un canale intero,
un pomeriggio); e **nessuna delle due batte i versamenti**, che restano il vincolo binding come il
26/07 aveva gia' stabilito.
---
## 5. AUTOCRITICA DELL'ONDATA
### 5.1 — 🚨 Un filone su venti non e' nel ledger, ed e' quello con il risultato eseguibile oggi
**ALT-OPTIONS (filone 14, `r0822_alt_options.py`, 709 righe) non ha ne' una riga di tabella ne'
una sezione in `RESULTS-0822.md`.** [VERIFICATO: 19 sezioni `###`, 18 righe di tabella, nessuna
per il 14 — e la tabella ha anche un difetto di markdown che **fonde le righe 9 e 4 su una sola
linea** (`... = TP01 travestito || 4 | DEALER-GAMMA | ...`).] Il filone e' citato **due volte** da
un altro filone (*«vedi filone 14»*, *«ETH_USDC resta la gamba giusta ... (filone 14)»*) e serve
al coordinatore per **ritirare una propria inferenza sbagliata** sull'OI — ma il lettore non trova
la fonte da nessuna parte.
Nel diario compare come **due parole** dentro l'elenco dei morti: *«alt-options (alt)»*. **La
sintesi e' sbagliata due volte:** il filone non muore «perche' alt» (la sua conclusione e' che
**SOL/XRP/HYPE/AVAX sono i peggiori**: SOL_USDC ha il **9%** dei put quotati a due lati e
`f_venue` **negativo**), e cio' che trova e' che **ETH_USDC** — non un alt — e' la gamba con
`f_venue = 0,980`, spread della corta al **2,6%**, **max-loss $16,62/lotto** e **3.000 lotti al
best bid**. Cioe' §1.7: **la quarta soglia pubblicata falsificata dall'ondata**, quella che tiene
VRP01 fuori per taglia — e il diario ne annuncia **tre**.
**Lezione:** il ledger e' stato scritto *man mano* (lo dice la sua prima riga) e un filone che
finisce mentre si scrive la sintesi puo' sparire senza che nulla lo segnali. Serve una guardia
banale: **contare i file `r0822_*.py` e le sezioni del ledger, e non chiudere l'ondata se non
combaciano.** E' lo stesso principio che l'ondata ha appena raccomandato per i monitor.
### 5.2 — Il gate `implausible_sharpe` non e' stato girato dove serviva, per la seconda volta
MONITOR-AUDIT lo osserva bene: *«`implausible_sharpe` esiste dal 26/07 e non e' mai stato puntato
sulle serie forward»* (avrebbe segnalato Sharpe −15,8 su 28 barre). Ma la stessa mancanza si
ripete **dentro l'ondata**: il gate non compare in PROP-ALLOC (dove J e P(pass) sono costruiti su
percorsi bootstrap, non su una serie reale — legittimo, ma va dichiarato) ne' in GROWTH-POLICY, il
cui output include un **capitale mediano di $4,4e12** — che e' letteralmente la firma per cui il
gate e' stato scritto. L'agente ha fatto il controllo di plausibilita' **a mano** e l'ha riportato
con onesta'; **il gate del progetto non e' stato invocato.** Un gate che si applica a mano e' un
gate che un giorno non si applichera'.
### 5.3 — Due «difetti di dato» diagnosticati per ricostruzione con la sorgente a due minuti di distanza
E' il tema di §1.2 e §1.3, e vale come **critica di metodo dell'ondata**, non dei due agenti. Il
progetto ha codificato il 07/08: *«una fonte normativa citata in un commento di codice si verifica
come un numero»* — nata da una legge citata male in 5 file per settimane. Il 22/08 due filoni
dichiarano un difetto in un dato prodotto da `/opt/docker/cerbero-mcp` (**acceso, sulla stessa
macchina, e per giunta gia' usato da PythagorasGoal per Hyperliquid**) e da
`Adriano/Cerbero-Bite` (su Gitea, ultimo commit di dismissione), **senza aprire ne' l'uno ne'
l'altro**. Io li ho aperti entrambi in **meno di dieci minuti**. Il brief e' complice: elenca le
dieci trappole statistiche e **non dice «prima di dichiarare un difetto di dato, leggi il codice
che quel dato lo produce»**. E' la regola nuova piu' economica che questa ondata possa lasciare.
### 5.4 — Il coordinatore ha lasciato passare una conclusione debole, e ne ha corretta una propria
**Corretta (a merito):** l'inferenza *«BTC_USDC e' praticamente morto»*, ritirata dopo la misura di
ALT-OPTIONS, con la ragione scritta accanto. E' il comportamento giusto.
**Lasciata passare:** la lezione di ORTHO-SCREEN (§2.1). E' etichettata *«il risultato piu'
riusabile dell'ondata»* e *«non e' sfortuna, e' aritmetica»* — due formule che chiudono la
discussione — mentre la sua meta' operativa (*«dichiarare famiglie molto piu' piccole in
anticipo»*) e' **la manovra che il progetto ha proibito il 30/07** e la sua meta' quantitativa
dipende da una variabile (la dispersione delle celle) **mai nominata**. Il segnale che avrebbe
dovuto fermarla c'era ed era **nello stesso ledger, sette sezioni piu' avanti**: il DSR vacuo di
VOL-SIZE. **Due misure dello stesso gate, versi opposti, stesso giorno, nessun rimando.**
La sintesi di venti filoni ha un compito che nessun filone ha: **incrociare**. Qui non l'ha fatto,
e non l'ha fatto neanche su §2.4 (leva), §2.5 (peso SKH01), §1.5 (le due gambe del gate 23/10) e
§1.6 (k\* contro la partecipazione al nastro) — **cinque incroci disponibili a costo zero, tutti
fra coppie di filoni gia' consegnati.**
### 5.5 — Agenti che hanno dichiarato «non girabile» cio' che era girabile
- **FLOW-SQUEEZE**: *«liquidation_long/short: NO ... Nessuna fonte nostra la produce, e non c'e'
niente da produrre»*. I due input della regola sono **nel file** (§1.3). Costo del mancato
lavoro: ~due ore. Il verdetto non sarebbe cambiato; la **motivazione** si'.
- **DEALER-GAMMA**: *«Quale [convenzione], lo decide la sezione 2 (ricostruzione dalla catena),
non io»* — decidibile leggendo, in due minuti (§1.2).
- **PROP-ALLOC**: la banda d'ancora dichiarata *«non girata, fuori budget»*. Onesto, e la
distorsione e' dichiarata a proprio sfavore. **Ma e' l'unica distorsione dichiarata che punta
nella direzione della raccomandazione**, sull'unico filone che raccomanda qualcosa: dovrebbe
essere il primo pezzo di budget di un seguito, non l'ultimo.
- **TERM-STRUCTURE**: *«`deflated_sharpe` NON girato, e la motivazione e' il risultato»* — questo
invece e' **corretto e da imitare**: con 74 osservazioni l'incertezza di campione domina di un
ordine di grandezza quella da selezione, e deflazionare 312 trial avrebbe dato *«un numero
preciso e privo di senso»*. **E' anche la migliore descrizione, scritta senza saperlo, del
problema di §2.1.**
### 5.6 — Cosa l'ondata ha fatto meglio della media del progetto (perche' un'autocritica che elenca
solo difetti non e' calibrata)
Sette agenti hanno catturato un **errore proprio** prima di pubblicare; tre hanno **refutato una
propria ipotesi in corsa**; uno scettico ha **ritirato una regola pubblicata poche ore prima**
provando che il difetto non esisteva — e la regola ritirata era **sua di famiglia**. Quattro
filoni hanno dichiarato la **potenza prima di guardare** (OI-PIN, FLOW-SQUEEZE, SLIP-AUDIT,
VOL-SIZE) e in tre casi il candidato e' morto **contro la propria soglia dichiarata**, che e' il
modo piu' pulito di morire. Le repliche **bit-exact prima di ogni delta** sono ormai
sistematiche (5/5 in XS-LITE e HL-EXEC, `max|diff| = 0,0`). **Il metodo dei singoli filoni non e'
il problema di questa ondata: la cucitura lo e'.**
---
## Appendice — cosa ho verificato in questa sessione, e come
| # | fatto | come |
|---|---|---|
| A | `dealer_net_gamma` = convenzione **dichiarata** (dealer short calls / long puts) nel sorgente di `cerbero-mcp`, non un difetto | letto `/opt/docker/cerbero-mcp/src/cerbero_mcp/common/options.py:11-14, 138-140` |
| B | bite **inoltra** il campo e non lo calcola; chiama con `top_n_strikes=50` di default | clone `Adriano/Cerbero-Bite`, `runtime/market_snapshot_cycle.py:85`, `clients/deribit.py:342-384` |
| C | `liquidation_*_risk` = soglia su `oi_delta_24h` + funding, e **i due input sono nel parquet** (copertura 99%, `oi_delta_pct_4h` std 1,23%) | `sentiment/fetchers.py:480-518` + lettura di `cb_market_snapshots.parquet` |
| D | `sr0` del DSR = `sd(Sharpe dei trial) x mult(N)`; con sd=0,58, `sr0` passa da **1,57 (N=168)** a **1,15 (N=24)** | letto `altlib.deflated_sharpe:689-712`, moltiplicatori ricalcolati |
| E | il libro 75/25 fa **+0,97%** il 12/03/2020 (BTC −41,5%, ETH −45,7%); peggior giorno **−3,38%**, non un crash | `altlib.tp01_baseline_daily()` + `sleeves._skyhook_returns()`, close-only |
| F | `collect_chain.py` interroga `{"currency": asset}` e filtra `OI_MIN = 100.0` | `scripts/live/collect_chain.py:57, 114, 181` |
| G | il feed certificato e' `BTC/USD:BTC` (**inverse**), il libro esegue `BTC_USDC-PERPETUAL` (**lineare**) | `rebuild_history.DERIBIT_INSTR:48` vs `src/live/deribit.INSTRUMENT:110` |
| H | ALT-OPTIONS: ETH_USDC `f_venue` 0,980 / max-loss **$16,62** / 7 strutture al 20% di $635 | output del filone in scratchpad |
| I | il dataset dei futures datati (**20 MB**) vive solo in `scratchpad/basis_cache/` | `du -sh` |
| J | `RESULTS-0822.md`: 19 sezioni, 18 righe di tabella, **filone 14 assente**, righe 9 e 4 fuse | conteggio diretto |
⚠️ **Su (G):** e' un fatto, non un'accusa. `rebuild_history` lo dichiara in un commento
(*«inverse, storia lunga ~ lineare entro 3 bps»*) e SLIP-AUDIT ha misurato la base ai 18 fill
(BTC −0,03 bps, ETH −0,27) — trascurabile sui **ritorni**, che dipendono dalle *variazioni* della
base. Lo segno solo perche' e' la **5ª occorrenza nell'ondata** dello schema «un controllo puntato
su uno strumento diverso da quello che si trada» (dopo `fee_watch` 21/08, il collettore della
catena, il feed dello slippage e la taratura di `venue_watch`), e perche' la parte non verificata
di quel commento — *«~ lineare entro 3 bps»* su **tutta la storia** — non e' mai stata misurata:
esiste una misura su 18 istanti, non sulle 60.000 ore in cui entrambi gli strumenti esistono.
Costo di chiuderla: un'ora.
---
**Book, pesi, cron, config: questo filone non tocca niente.** Consegna un'analisi.
Le uniche azioni che raccomanda sono a costo quasi nullo e tutte su documenti o verifiche:
correggere §1.2 nel ledger, aggiungere la sezione mancante di §5.1, dichiarare la dispersione
accanto a ogni DSR (§2.1), ri-registrare il gate del 23/10 prima di rigenerare le serie (§1.4-1.5),
persistere il dataset dei futures (§3.4), e leggere il listino di una prop firm prima di
misurare un'altra volta il canale che ne dipende (§1.1).
+811
View File
@@ -0,0 +1,811 @@
"""r0822b_depeg.py — IL FALSIFICATORE DI VENUE PER XS01-OOS (2026-08-22, ondata wave-0822).
IL PUNTO FRAGILE, DICHIARATO DALL'AUTORE DEL RISULTATO
-----------------------------------------------------
`r0822b_xs01_oos.py` (RESULTS-0822 §21) ha misurato che il meccanismo congelato di XS01 ha un edge
FUORI dalla sua finestra di scoperta — 2021-2023 su **Binance spot USDT** — e che li' e' PIU' GRANDE
(mediana di fase +1,12 contro +0,37, differenza appaiata +0,668 in 10/10 fasi). Quel risultato
riscrive il gate del canale funded, quindi merita il suo falsificatore.
Lo stesso autore lo ha nominato:
> "La validazione di venue Binance-vs-Hyperliquid gira SOLO sulla finestra comune 2024+, cioe'
> proprio il periodo SENZA depeg USDT. Il fuori campione contiene il depeg di maggio 2022. Se i
> close Binance/USDT 2021-2023 divergessero da un venue USD in modo NON COMUNE fra gli asset, il
> ranking cambierebbe e il risultato con esso. Test che lo deciderebbe: rifare il meccanismo sui
> close Coinbase USD per il sottoinsieme quotato li', 2021-2023, e chiedere corr fra i due sleeve
> >= 0.99 come sul 2024+."
Questo script esegue esattamente quel test.
PERCHE' LA DOMANDA NON E' OZIOSA (il meccanismo del danno, prima dei numeri)
---------------------------------------------------------------------------
La regola del progetto — "Binance non e' la verita', e' USDT, ~10 bps fuori, e sotto depeg fino al
3% off" — e' stata **correttamente** giudicata non-binding da §21: uno z-score cross-sezionale
sottrae la media di sezione, quindi e' invariante a un fattore **COMUNE**. Ma un depeg non e'
garantito comune: nei giorni di stress la liquidita' si sposta in modo diverso per asset (i major
restano scambiati contro USDT, i minori no; i flussi di fuga passano per coppie diverse), e una
divergenza **IDIOSINCRATICA** cambia il RANKING — che e' esattamente cio' che la strategia legge.
Maggio 2022 (UST/LUNA, con USDT sceso a ~0,95) e' il caso peggiore possibile.
Percio' la divergenza va misurata in DUE pezzi, e sono due domande diverse:
(a) COMUNE = mediana per giorno dello scarto fra i venue -> quella che lo z-score annulla;
(b) IDIOSINCRATICA = scarto per asset DOPO aver tolto la mediana del giorno -> quella che uccide.
Riportare solo (a) darebbe la risposta rassicurante e sbagliata.
IL VINCOLO DEL DATO, E COME E' STATO RISOLTO (dichiarato PRIMA di misurare)
--------------------------------------------------------------------------
Su Coinbase USD, dei 13 asset di U13:
BNB -> quotato solo dal 2025-10 = FUORI (nessun venue USD ce l'ha nel 2021-23: e' l'asset
nativo del venue che stiamo mettendo in discussione)
XRP -> SOSPESO su Coinbase dal 2021-01-19 al 2023-07 (causa SEC) = buco proprio sulla finestra
NEAR/INJ -> quotati su Coinbase solo dal 2022-09
AVAX -> dal 2021-09-30 (il vincolo che decide l'inizio della finestra)
XRP e' recuperato da **Bitstamp USD** (l'altra referenza USD del `venue_watch` del progetto, che
non ha mai sospeso la coppia). Restano fuori BNB, NEAR, INJ.
Ne escono DUE test, e il secondo copre cio' che il primo non copre:
TEST 1 "U10-USD" = universo FISSO di 10 gambe (BTC ETH LTC LINK AAVE ADA DOGE SOL AVAX + XRP),
2021-09-30 -> 2023-12-31, meccanismo **CONGELATO** (k=5, min_assets=10:
dieci gambe sono ESATTAMENTE il minimo, nessun parametro toccato).
Contiene il depeg di maggio 2022 e FTX. NON contiene il 2021.
TEST 2 "U13-IBRIDO" = i 13 asset e la finestra INTERA 2021-01 -> 2023-12, con il close USD
sostituito a quello USDT **dove esiste** (9-12 gambe su 13 a seconda della
data) e Binance dove non esiste. Non e' un confronto puro di venue, e'
la domanda operativa: *se sostituisco tutto il prezzo USD che esiste, il
risultato di §21 si muove?*
In entrambi i test il confronto e' **APPAIATO**: stesso sottoinsieme, stessa finestra, stesse 10
fasi, stesso codice del motore. Confrontare "19 gambe su Binance" con "10 su Coinbase" misurerebbe
l'UNIVERSO, non il venue (regola del progetto: un confronto appaiato eredita TUTTI i parametri
della riga in cui compare) — e per questo il TEST 1 riporta anche cosa fa U10 **su Binance**
rispetto a U13 su Binance, cioe' quanto del titolo di §21 questo sottoinsieme riproduce.
MECCANISMO: CONGELATO. Il motore e' importato da `r0822b_xs01_oos.py` (che a sua volta replica
`sleeves._xsec_returns` a max|diff| = 0.0). Nessuna riga di strategia riscritta qui: se il motore
fosse riscritto, una differenza di venue e una differenza di implementazione sarebbero
indistinguibili.
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_depeg.py
Rete: sola lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col
collettore della catena opzioni; guasto del 29/07). Download in cache nello scratchpad: la
seconda corsa e' offline.
"""
from __future__ import annotations
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"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
import r0822b_xs01_oos as X # noqa: E402 (motore CONGELATO + cache Binance)
from src.portfolio.sleeves import XS_CFG # noqa: E402
CACHE = X.CACHE.parent / "r0822b_depeg"
CACHE.mkdir(parents=True, exist_ok=True)
# ---------------------------------------------------------------------------
# UNIVERSI E FINESTRE — dichiarati PRIMA di guardare i numeri
# ---------------------------------------------------------------------------
U13 = X.U13 # l'universo di §21
# venue USD per asset. `None` = nessun venue USD con storia sulla finestra.
USD_SRC: dict[str, tuple[str, str] | None] = {
"BTC": ("coinbase", "BTC/USD"), "ETH": ("coinbase", "ETH/USD"),
"LTC": ("coinbase", "LTC/USD"), "LINK": ("coinbase", "LINK/USD"),
"AAVE": ("coinbase", "AAVE/USD"), "ADA": ("coinbase", "ADA/USD"),
"DOGE": ("coinbase", "DOGE/USD"), "SOL": ("coinbase", "SOL/USD"),
"AVAX": ("coinbase", "AVAX/USD"), "NEAR": ("coinbase", "NEAR/USD"),
"INJ": ("coinbase", "INJ/USD"),
"XRP": ("bitstamp", "XRP/USD"), # Coinbase l'ha sospeso 2021-01 -> 2023-07
"BNB": None, # nessun venue USD prima del 2025-10
}
U10 = ["BTC", "ETH", "LTC", "LINK", "AAVE", "ADA", "DOGE", "SOL", "AVAX", "XRP"]
T1_START, T1_END = "2021-09-30", "2024-01-01" # AVAX su Coinbase e' il vincolo
T2_START, T2_END = "2021-01-01", "2024-01-01" # la finestra fuori campione di §21
DEPEG = ("2022-05-07", "2022-05-21") # UST/LUNA + USDT a ~0.95 (estremo incluso)
PHASES = list(range(XS_CFG["H"])) # 10 fasi: la stima onesta e' la MEDIANA
CORR_SOGLIA = 0.99 # soglia DICHIARATA DALL'AUTORE, non da me
# ===========================================================================
# 1. FETCH USD (Coinbase / Bitstamp) + certificazione locale
# ===========================================================================
def _fetch_paged(ex, market: str, tf: str, start_ms: int, end_ms: int,
step_bars: int, bar_ms: int) -> list:
"""Paginazione DETERMINISTICA sulla finestra: avanza anche quando una pagina torna vuota.
⚠️ Non e' un dettaglio: XRP su Coinbase ha un buco di 2,5 anni (sospensione SEC) e un loop che
si ferma alla prima pagina vuota — il modo ovvio di scrivere questo ciclo — troncherebbe la
serie li', producendo un 'asset che non esiste' invece di un 'asset con un buco'. Sono due
fatti diversi e portano a due decisioni diverse.
"""
rows, cur, span = [], start_ms, step_bars * bar_ms
while cur < end_ms:
X._outside_forbidden_window()
batch = []
for attempt in range(3):
try:
batch = ex.fetch_ohlcv(market, tf, since=cur, limit=step_bars)
break
except Exception as e: # noqa: BLE001
if attempt == 2:
print(f" [rete] {market} @ {cur}: {type(e).__name__} {str(e)[:70]}")
time.sleep(2 ** attempt)
time.sleep(0.55) # <= 2 req/s
rows += [b for b in (batch or []) if cur <= b[0] < cur + span]
cur += span
return rows
def fetch_usd_daily(asset: str) -> pd.DataFrame:
"""Close giornalieri USD dal venue dichiarato in USD_SRC. Cache su disco."""
src = USD_SRC.get(asset)
if src is None:
return pd.DataFrame(columns=["timestamp", "close", "volume"])
venue, market = src
f = CACHE / f"{venue[:2]}_{asset.lower()}_1d.parquet"
if f.exists():
return pd.read_parquet(f)
import ccxt
ex = getattr(ccxt, venue)({"enableRateLimit": True})
ex.load_markets()
if market not in ex.markets:
d = pd.DataFrame(columns=["timestamp", "open", "high", "low", "close", "volume"])
d.to_parquet(f)
return d
start = ex.parse8601("2019-01-01T00:00:00Z")
end = int(time.time() * 1000)
step = 300 if venue == "coinbase" else 1000
rows = _fetch_paged(ex, market, "1d", start, end, step, 86_400_000)
d = (pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True))
d.to_parquet(f)
return d
def series_of(d: pd.DataFrame) -> pd.Series:
if not len(d):
# indice tz-aware anche da vuoto: un asset senza venue USD (BNB) deve poter essere
# confrontato/reindicizzato come gli altri, non far cadere lo script
return pd.Series(dtype=float, index=pd.DatetimeIndex([], tz="UTC"))
return pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
def certify_usd(asset: str, d: pd.DataFrame, a: str, b: str) -> dict:
"""Stessa forma della certificazione di §21, ma valutata SULLA FINESTRA DEL TEST.
Certificare "tutta la serie" direbbe la cosa sbagliata: XRP su Coinbase ha 900 giorni di buco
che sulla finestra 2021-09/2023-12 sono TUTTI dentro. Il numero utile e' la copertura della
finestra in cui il meccanismo gira, non della serie.
"""
s = series_of(d)
s = s[(s.index >= pd.Timestamp(a, tz="UTC")) & (s.index < pd.Timestamp(b, tz="UTC"))]
if not len(s):
return dict(sym=asset, n=0, ok=False, start="-", end="-", gap=-1,
flat=float("nan"), zero_vol=float("nan"))
v = series_of(d.assign(close=d["volume"]))
v = v[(v.index >= pd.Timestamp(a, tz="UTC")) & (v.index < pd.Timestamp(b, tz="UTC"))]
# ⚠️ la copertura si misura sulla finestra RICHIESTA, non sulla prima barra disponibile:
# contando dal proprio inizio, un asset quotato a meta' finestra esce con gap=0 e "OK", e il
# conteggio degli usabili contraddirebbe la riga sotto che lo esclude. E' lo stesso difetto
# che il progetto ha gia' incontrato due volte (serie TRONCATA che passa ogni controllo
# interno): un controllo che guarda solo DENTRO la serie non vede cio' che la serie non copre.
attesi = (pd.Timestamp(b, tz="UTC") - pd.Timestamp(a, tz="UTC")).days
c = s.values
flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan")
zv = float(np.mean(v.values == 0.0)) if len(v) else float("nan")
gap = int(attesi - len(s))
return dict(sym=asset, n=len(s), start=str(s.index.min().date()),
end=str(s.index.max().date()), gap=gap, flat=flat, zero_vol=zv,
ok=bool(gap == 0 and flat < 0.02 and (not np.isfinite(zv) or zv < 0.01)))
# ===========================================================================
# 2. pannelli
# ===========================================================================
def bin_panel(syms: list[str]) -> pd.DataFrame:
"""Pannello Binance USDT — dalla cache di §21, cosi' il lato USDT del confronto e' BIT-IDENTICO
a quello che ha prodotto il risultato che sto falsificando."""
return pd.concat({s: series_of(X.fetch_binance_daily(s)) for s in syms}, axis=1).sort_index()
def usd_panel(syms: list[str]) -> pd.DataFrame:
return pd.concat({s: series_of(fetch_usd_daily(s)) for s in syms}, axis=1).sort_index()
def splice(usd: pd.Series, usdt: pd.Series) -> pd.Series:
"""Serie IBRIDA: USD dove esiste, USDT prima — RI-SCALANDO il tratto USDT al livello USD nel
giorno del giunto.
⚠️ Senza la ri-scalatura il giunto crea un rendimento fantasma pari allo scarto fra i venue
(pochi bps, ma esattamente il tipo di artefatto che questo script deve misurare, non fabbricare).
Il meccanismo legge RAPPORTI di prezzo a 30 e 90 giorni: dopo la ri-scalatura la serie e'
continua e i rapporti che attraversano il giunto sono ben definiti.
"""
u = usd.dropna()
if not len(u):
return usdt
t0 = u.index[0]
if t0 not in usdt.index or not np.isfinite(usdt.get(t0, np.nan)):
return usd.reindex(usdt.index)
k = float(u.iloc[0]) / float(usdt.loc[t0])
pre = usdt[usdt.index < t0] * k
return pd.concat([pre, u]).sort_index().reindex(usdt.index)
def hybrid_panel(syms: list[str]) -> tuple[pd.DataFrame, dict]:
B, U = bin_panel(syms), usd_panel(syms)
cols, meta = {}, {}
for s in syms:
u = U[s] if s in U.columns else pd.Series(dtype=float)
u = u.reindex(B.index)
cols[s] = splice(u, B[s])
cov = u.dropna()
meta[s] = (str(cov.index[0].date()) if len(cov) else "mai")
return pd.concat(cols, axis=1).sort_index(), meta
# ===========================================================================
# 3. metriche e allineamento appaiato
# ===========================================================================
sh, dd, cagr, win, alive, line = X.sh, X.dd, X.cagr, X.win, X.alive, X.line
def pair(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]:
"""Allinea due sleeve sullo stesso indice: un confronto di venue non puo' confrontare finestre."""
j = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return j["a"], j["b"]
def ranking_scan(PB: pd.DataFrame, PU: pd.DataFrame, phases: list[int],
label: str) -> pd.DataFrame:
"""IL CANALE DEL DANNO, misurato dove passa: il venue puo' far male SOLO cambiando le gambe
scelte. Scansione di TUTTE le fasi (non solo la canonica: un ribaltamento su una fase sola
sarebbe invisibile all'ancora 0) con self-check dello specchio contro il motore."""
from scipy.stats import spearmanr
lb = XS_CFG["lookbacks"]
mlb, H, k = max(lb), XS_CFG["H"], XS_CFG["k"]
rows, sp, checked, tot = [], [], 0, 0
for ph in phases:
det: dict = {}
X.xsec_engine(PB, phase=ph, detail=det)
WB = det["W"]
for i in range(mlb, len(PB)):
if i % H != ph:
continue
iB, sB = score_at(PB, i, lb)
iU, sU = score_at(PU, i, lb)
if len(iB) < 10 or len(iU) < 10:
continue
tot += 1
oB = np.argsort(sB)
loB, hiB = set(iB[oB[:k]]), set(iB[oB[-k:]])
mh, ml = set(np.flatnonzero(WB[i] > 0)), set(np.flatnonzero(WB[i] < 0))
if mh or ml: # self-check solo dove il motore ha una posizione
checked += 1
if (mh, ml) != (hiB, loB):
raise AssertionError(f"specchio != motore ({label}, fase {ph}, i={i}): "
"misurerei la mia riscrittura invece del venue")
oU = np.argsort(sU)
loU, hiU = set(iU[oU[:k]]), set(iU[oU[-k:]])
ov = (len(hiB & hiU) + len(loB & loU)) / (2 * k)
sp.append(float(spearmanr(sB, sU).statistic))
if ov < 1.0:
diff = sorted(set(PB.columns[list(hiB ^ hiU)]) | set(PB.columns[list(loB ^ loU)]))
rows.append(dict(fase=ph, data=PB.index[i], overlap=ov, gambe=",".join(diff)))
D = pd.DataFrame(rows, columns=["fase", "data", "overlap", "gambe"])
print(f" [{label}] {tot} ribilanciamenti su {len(phases)} fasi; specchio verificato "
f"contro il motore in {checked}/{checked} con posizione")
print(f" [{label}] Spearman dello score fra i venue: mediana {np.median(sp):.4f} "
f"min {np.min(sp):.4f}")
print(f" [{label}] ribilanciamenti con gambe DIVERSE: {len(D)}/{tot} "
f"({len(D)/max(tot,1)*100:.2f}%)")
if len(D):
pre = D[D["data"] < pd.Timestamp(T2_END, tz="UTC")]
print(f" [{label}] di cui NELLA finestra fuori campione ({T2_START}..{T2_END}): "
f"{len(pre)} — elencati qui sotto; quelli dal 2024 in poi non sono la domanda")
for _, r in pre.iterrows():
print(f" fase {int(r['fase'])} {r['data'].date()} "
f"sovrapposizione {r['overlap']*100:3.0f}% gambe scambiate: {r['gambe']}")
return D
def gate_flips(PB: pd.DataFrame, PU: pd.DataFrame, ph: int) -> list[tuple]:
"""Il SECONDO canale, che una scansione di ranking non vede: il gate di dispersione e' BINARIO
(`disp_i >= percentile espandente`) e quindi DISCONTINUO — due prezzi che differiscono di
pochi bps possono cadere ai due lati della soglia, e allora uno sleeve resta pienamente
investito per 10 giorni mentre l'altro sta FLAT. Non e' un cambio di ranking: e' un
interruttore. Ritorna gli intervalli in cui i due venue non sono d'accordo su acceso/spento."""
dB: dict = {}
dU: dict = {}
X.xsec_engine(PB, phase=ph, detail=dB)
X.xsec_engine(PU, phase=ph, detail=dU)
gb = np.abs(dB["W"]).sum(axis=1) > 1e-9
gu = np.abs(dU["W"]).sum(axis=1) > 1e-9
idx = PB.index
flip = np.flatnonzero(gb != gu)
runs, i0 = [], None
for j, i in enumerate(flip):
if i0 is None:
i0 = i
if j + 1 == len(flip) or flip[j + 1] != i + 1:
runs.append((idx[i0], idx[i], int(i - i0 + 1), bool(gb[i0])))
i0 = None
return runs
def autopsia(PB: pd.DataFrame, PU: pd.DataFrame, ph: int, D: pd.DataFrame, label: str) -> None:
"""La fase con la corr piu' bassa NON si riassume, si guarda: una corr di 0,97 o e' un
ribaltamento di ranking (il guasto che sto cercando) o e' un singolo giorno di rumore su una
gamba sottile (che non e' un guasto). Sono conclusioni opposte a partire dallo stesso numero."""
a, b = pair(alive(X.xsec_engine(PU, phase=ph)), alive(X.xsec_engine(PB, phase=ph)))
g = (a - b)
top = g.abs().sort_values(ascending=False).head(4)
dph = D[D["fase"] == ph] if len(D) else D
runs = gate_flips(PB, PU, ph)
print(f" autopsia della fase peggiore ({label}, fase {ph}): i 4 giorni di scarto maggiore")
for t, v in top.items():
prev = dph[dph["data"] <= t]["data"]
near = (t - prev.iloc[-1]).days if len(prev) else None
inflip = [r for r in runs if r[0] <= t <= r[1]]
# ⚠️ la coda: dopo una discordanza del gate uno dei due sleeve ha 10 giorni di ZERI in
# piu' nella finestra rv30 del vol-target -> la LEVA dei due resta diversa per ~30 giorni
# dopo che la discordanza e' finita. Chiamare "rumore di prezzo" un giorno dentro quella
# coda attribuirebbe al venue un effetto che e' della soglia binaria.
tail = [r for r in runs if r[1] < t and (t - r[1]).days <= 30]
if inflip:
r = inflip[0]
tag = (f"GATE DISCORDE {r[0].date()}..{r[1].date()}: "
f"{'USDT investito, USD flat' if r[3] else 'USD investito, USDT flat'}")
elif tail:
tag = (f"coda del vol-target: gate discorde fino al {tail[-1][1].date()}, "
f"{(t - tail[-1][1]).days}g fa -> rv30 diversa, LEVA diversa")
elif near is not None and near <= XS_CFG["H"]:
tag = f"ultimo ribilanciamento con gambe diverse {near} giorni prima"
else:
tag = "stesse gambe, stesso gate: solo rumore di prezzo"
print(f" {t.date()} scarto {g.loc[t]*1e4:+8.0f} bps ({tag})")
print(f" scarto quadratico totale della fase: {float((g**2).sum())**0.5*1e4:.0f} bps; "
f"i 4 giorni sopra ne spiegano "
f"{float((top**2).sum())/float((g**2).sum())*100:.0f}%")
def score_at(P: pd.DataFrame, i: int, lookbacks) -> tuple[np.ndarray, np.ndarray]:
"""Specchio della riga di score del motore (z-score medio sui lookback). Serve per confrontare
i RANKING fra venue; la sezione [4] verifica che i top-k/bottom-k che ne derivano coincidano
ESATTAMENTE con quelli scelti dal motore — se lo specchio divergesse, ogni numero di ranking
misurerebbe la mia riscrittura invece del venue."""
px = P.values.astype(float)
elig = np.isfinite(px[i])
for L in lookbacks:
elig = elig & np.isfinite(px[i - L])
idx = np.flatnonzero(elig)
sc, cnt = np.zeros(len(idx)), 0
for L in lookbacks:
r = px[i, idx] / px[i - L, idx] - 1.0
if r.std() > 0:
sc += (r - r.mean()) / r.std()
cnt += 1
return idx, (sc / cnt if cnt else sc)
# ===========================================================================
def main() -> None:
t0 = time.time()
pd.set_option("display.width", 200)
print("=" * 100)
print("DEPEG — il close USDT 2021-2023 puo' aver cambiato il RANKING di XS01-OOS?")
print(f"XS_CFG (CONGELATO) = {XS_CFG} soglia dichiarata dall'autore: corr >= {CORR_SOGLIA}")
print("=" * 100)
# ------------------------------------------------------------- [0] sanity
print("\n[0] SANITY — il motore importato e' quello che ha prodotto §21?")
from src.portfolio.sleeves import _xsec_returns
off = _xsec_returns()
rep = X.xsec_engine(X.hl_panel(), phase=0)
j = pd.concat({"o": off, "r": rep}, axis=1, join="inner").dropna()
md = float(np.max(np.abs(j["o"] - j["r"])))
print(f" motore vs sleeve UFFICIALE su HL: n={len(j)} max|diff| = {md:.2e} "
f"-> {'IDENTICO' if md < 1e-12 else 'DIVERGE — STOP'}")
if md >= 1e-12:
return
# ------------------------------------------------------- [1] dato USD
print("\n[1] DATO USD — quanti dei 13 asset di §21 hanno un prezzo in DOLLARI nel 2021-2023?")
print(f" {'sym':<6}{'venue USD':>11}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}"
f"{'flat%':>8}{'vol0%':>8} (finestra {T1_START} .. {T1_END})")
certs = {}
for s in U13:
d = fetch_usd_daily(s)
c = certify_usd(s, d, T1_START, T1_END)
certs[s] = c
ven = USD_SRC[s][0] if USD_SRC[s] else "—"
flag = "" if c["ok"] else " <-- non usabile su questa finestra"
print(f" {s:<6}{ven:>11}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap']:>5}"
f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{flag}")
usabili = [s for s in U13 if certs[s]["ok"]]
print(f" -> usabili sulla finestra del TEST 1: {len(usabili)}/13 {usabili}")
print(f" -> ESCLUSI: BNB (nessun venue USD prima del 2025-10 — e' l'asset nativo del venue")
print(f" sotto esame), NEAR e INJ (Coinbase solo dal 2022-09).")
print(f" ⚠️ LIMITE DEL TEST, da dire subito: 10 gambe su 13 e finestra che parte dal")
print(f" {T1_START} (AVAX su Coinbase) -> il TEST 1 NON copre il 2021, che in §21 e'")
print(f" l'anno piu' forte (+2,14). Copre pero' ESATTAMENTE cio' che l'obiezione nomina:")
print(f" il depeg di maggio 2022 e FTX. Il 2021 lo copre il TEST 2 (ibrido).")
# controllo di ALLINEAMENTO: le barre USD sono sulla stessa griglia di quelle USDT?
print("\n Controllo di ALLINEAMENTO — se le barre USD e USDT fossero su griglie sfasate,")
print(" ogni numero sotto sarebbe uno sfasamento di calendario travestito da divergenza")
print(" di venue. corr dei RITORNI giornalieri, 2024+ (finestra dove esistono entrambi):")
B24 = bin_panel(U13)
U24 = usd_panel(U13)
al, shifted = [], []
for s in U13:
if s not in U24.columns or not U24[s].notna().any():
continue
k = pd.concat({"u": U24[s], "b": B24[s]}, axis=1, join="inner").dropna()
k = k[k.index >= pd.Timestamp("2024-01-01", tz="UTC")].pct_change().dropna()
if len(k) > 200:
al.append((s, float(k["u"].corr(k["b"]))))
shifted.append(float(k["u"].shift(1).corr(k["b"]))) # controllo POSITIVO
print(" " + " ".join(f"{s}:{c:.4f}" for s, c in al))
lo_al = min(c for _, c in al)
# ⚠️ Un controllo che non fallisce mai non e' un controllo: il modo di fallire e' uno
# sfasamento di UNA barra, e va mostrato che il numero se ne accorge. Con la serie USD
# ritardata di un giorno la corr collassa: e' quella la scala del guasto che sto escludendo.
print(f" corr minima {lo_al:.4f} | controllo POSITIVO (USD ritardato di 1 giorno): "
f"corr mediana {np.median(shifted):+.4f}, massima {np.max(shifted):+.4f}")
if lo_al < 0.95:
print(" -> SFASAMENTO: i due venue non sono sulla stessa griglia. STOP.")
return
print(" -> allineati (uno sfasamento darebbe ~0, non 0,99). Gli asset sotto 0,999")
print(f" ({', '.join(s for s, c in al if c < 0.999)}) sono solo piu' RUMOROSI in USD")
print(" (book piu' sottile sul venue USD), non sfasati.")
# --------------------------------------------- [2] la divergenza, in DUE pezzi
print("\n[2] LA DIVERGENZA USD-vs-USDT, scomposta — e il pezzo che conta e' il SECONDO")
Bp = bin_panel(U10).loc[T1_START:T1_END]
Up = usd_panel(U10).reindex(Bp.index)
both = Bp.notna() & Up.notna()
D = 1e4 * np.log(Up / Bp) # scarto di livello, bps
D = D.where(both)
common = D.median(axis=1) # (a) componente COMUNE (il peg + il venue)
idio = D.sub(common, axis=0) # (b) componente IDIOSINCRATICA (cambia il rango)
print(f" {len(Bp)} giorni, {len(U10)} asset, {int(both.values.sum())} osservazioni appaiate")
print(f" (a) COMUNE |mediana per giorno|: med {common.abs().median():7.1f} bps "
f"p95 {common.abs().quantile(.95):7.1f} max {common.abs().max():7.1f}")
print(f" (b) IDIOSINCRATICO |scarto de-medianato|: med {idio.abs().stack().median():7.1f} bps "
f"p95 {idio.abs().stack().quantile(.95):7.1f} max {idio.abs().stack().max():7.1f}")
print(" -> (a) e' cio' che lo z-score cross-sezionale annulla per costruzione; (b) no.")
print(f"\n LA FINESTRA DEL DEPEG {DEPEG[0]} .. {DEPEG[1]} (UST/LUNA; USDT a ~0,95):")
dm = (D.index >= pd.Timestamp(DEPEG[0], tz="UTC")) & (D.index < pd.Timestamp(DEPEG[1], tz="UTC"))
print(f" {'giorno':<12}{'comune':>9}" + "".join(f"{s:>8}" for s in U10))
for t in D.index[dm]:
print(f" {str(t.date()):<12}{common.loc[t]:>9.0f}" +
"".join(f"{idio.loc[t, s]:>8.0f}" if np.isfinite(idio.loc[t, s]) else f"{'—':>8}"
for s in U10))
print(" (numeri in bps; colonna 'comune' = mediana del giorno, le altre = scarto DOPO")
print(" averla tolta, cioe' la parte che puo' spostare il ranking)")
idm = idio[dm].abs().stack()
print(f" idiosincratico NELLA finestra: med {idm.median():.1f} bps p95 "
f"{idm.quantile(.95):.1f} max {idm.max():.1f} "
f"(FUORI: med {idio[~dm].abs().stack().median():.1f}, "
f"p95 {idio[~dm].abs().stack().quantile(.95):.1f})")
print("\n Ma il ranking non legge il LIVELLO: legge i rapporti a 30 e 90 giorni. Divergenza")
print(" idiosincratica degli STESSI rendimenti che entrano nello score (bps):")
for L in XS_CFG["lookbacks"]:
rB = (Bp / Bp.shift(L) - 1.0) * 1e4
rU = (Up / Up.shift(L) - 1.0) * 1e4
dR = (rU - rB).where(both)
dRi = dR.sub(dR.median(axis=1), axis=0)
w = dRi[dm].abs().stack()
print(f" r{L:<3d}: idiosincratico med {dRi.abs().stack().median():6.1f} bps "
f"p95 {dRi.abs().stack().quantile(.95):7.1f} max {dRi.abs().stack().max():8.1f}"
f" | nel depeg med {w.median():6.1f} max {w.max():8.1f}")
# ------------------------------------------- [3] IL TEST 1 — U10, k=5 congelato
print(f"\n[3] TEST 1 — meccanismo CONGELATO su U10, {T1_START} .. {T1_END}, USD vs USDT")
print(f" 10 gambe = ESATTAMENTE il minimo del meccanismo (min_assets=10, k=5): nessun")
print(f" parametro toccato, e il confronto e' appaiato su TUTTO tranne il venue.")
PB = Bp[U10].dropna()
PU = Up[U10].dropna()
idxc = PB.index.intersection(PU.index)
PB, PU = PB.loc[idxc], PU.loc[idxc]
print(f" pannello comune: {len(idxc)} giorni, {PB.index[0].date()} .. {PB.index[-1].date()}")
rows = []
for ph in PHASES:
a = alive(X.xsec_engine(PU, phase=ph))
b = alive(X.xsec_engine(PB, phase=ph))
a, b = pair(a, b)
rows.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b),
d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b), n=len(a)))
R = pd.DataFrame(rows)
print(f" {'fase':>5}{'corr':>9}{'Sh USD':>9}{'Sh USDT':>9}{'dSharpe':>10}"
f"{'DD USD':>9}{'DD USDT':>9}")
for _, r in R.iterrows():
print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>9.2f}{r['sh_usdt']:>9.2f}"
f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%")
print(f" {'MEDIANA':>5}{R['corr'].median():>9.4f}{R['sh_usd'].median():>9.2f}"
f"{R['sh_usdt'].median():>9.2f}{R['d'].median():>+10.3f}"
f"{R['dd_usd'].median()*100:>8.1f}%{R['dd_usdt'].median()*100:>8.1f}%")
print(f" corr MINIMA fra le 10 fasi: {R['corr'].min():.4f} (fase {int(R.loc[R['corr'].idxmin(),'ph'])})"
f" | soglia dichiarata {CORR_SOGLIA} -> "
f"{'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
print(f" dSharpe appaiato (USD - USDT): mediana {R['d'].median():+.3f} "
f"banda [{R['d'].min():+.3f},{R['d'].max():+.3f}] "
f"fasi in cui USD e' MEGLIO {(R['d'] > 0).sum()}/{len(R)}")
print(f" ⚠️ la statistica e' la MEDIANA DELLE DIFFERENZE APPAIATE, non la differenza delle")
print(f" mediane (lezione 26/07: quest'ultima confronta fasi diverse e puo' ribaltare).")
# quanto di §21 riproduce questo sottoinsieme? (o si misura l'universo, non il venue)
p13 = bin_panel(U13)[U13].dropna()
s13_w = win(alive(X.xsec_engine(p13, phase=0)), T1_START, T1_END)
s10_w = win(alive(X.xsec_engine(PB, phase=0)), T1_START, T1_END)
print(f"\n Contesto obbligatorio — quanto di §21 vive in questo sottoinsieme (Binance, fase 0,")
print(f" stessa finestra): U13 Sh {sh(s13_w):+.2f} | U10 Sh {sh(s10_w):+.2f}")
print(f" -> il TEST 1 giudica il VENUE su U10; NON e' una replica del titolo di §21.")
# ------------------------------------------------ [4] i RANKING, non i P&L
print("\n[4] IL CANALE DEL DANNO — cambia il RANKING? (e' cio' che la strategia legge)")
print(" Un P&L simile puo' nascere da due ranking diversi che si compensano: la domanda")
print(" diretta e' se le GAMBE SCELTE cambiano. Scansione su tutte e 10 le fasi.")
D1 = ranking_scan(PB, PU, PHASES, "TEST 1 / U10")
if len(D1):
ind = D1[(D1["data"] >= pd.Timestamp(DEPEG[0], tz="UTC")) &
(D1["data"] < pd.Timestamp(DEPEG[1], tz="UTC"))]
print(f" di cui dentro la finestra del depeg: {len(ind)}")
# ---------------------- [4b] il SECONDO canale: il gate binario, e l'attribuzione
print("\n[4b] IL GATE DI DISPERSIONE E' UN INTERRUTTORE — e una scansione di ranking non lo")
print(" vede. `disp_i >= percentile espandente` e' una soglia: pochi bps di differenza di")
print(" prezzo possono cadere ai due lati, e per 10 giorni uno sleeve e' pieno e l'altro")
print(" e' FLAT. E' l'unico modo in cui una differenza di venue microscopica puo'")
print(" produrre una differenza di P&L macroscopica.")
for label_, Pb_, Pu_ in (("TEST 1 / U10", PB, PU),):
tot_fl = 0
for ph in PHASES:
for a_, b_, nd, bfirst in gate_flips(Pb_, Pu_, ph):
tot_fl += 1
print(f" [{label_}] fase {ph}: {a_.date()} .. {b_.date()} ({nd}g) — "
f"{'USDT investito / USD FLAT' if bfirst else 'USD investito / USDT FLAT'}")
if tot_fl == 0:
print(f" [{label_}] nessuna discordanza del gate su 10 fasi")
else:
print(f" [{label_}] {tot_fl} intervalli di discordanza su 10 fasi")
print("\n ATTRIBUZIONE — stesso confronto con il gate SPENTO su ENTRAMBI i venue")
print(" (diagnostica, NON una proposta di cambiare il meccanismo: serve a separare")
print(" 'il prezzo e' diverso' da 'la soglia binaria amplifica una differenza minuscola'):")
cg = []
for ph in PHASES:
a_, b_ = pair(alive(X.xsec_engine(PU, phase=ph, disp_pct=0)),
alive(X.xsec_engine(PB, phase=ph, disp_pct=0)))
cg.append(float(a_.corr(b_)))
print(f" [TEST 1] corr con gate SPENTO: mediana {np.median(cg):.4f} minima {np.min(cg):.4f}"
f" (col gate acceso: mediana {R['corr'].median():.4f}, minima {R['corr'].min():.4f})")
# ------------------------------- [5] TEST 2 — U13 ibrido, finestra INTERA di §21
print(f"\n[5] TEST 2 — U13 IBRIDO (USD dove esiste, USDT altrove), {T2_START} .. {T2_END}")
print(" Domanda operativa: sostituendo TUTTO il prezzo in dollari che esiste, il titolo")
print(" di §21 (mediana di fase +1,12 fuori campione) si muove?")
HB, meta = hybrid_panel(U13)
print(" prima data USD per asset: " + " ".join(f"{s}:{meta[s]}" for s in U13))
PBh = p13
PUh = HB[U13].dropna()
ic = PBh.index.intersection(PUh.index)
PBh, PUh = PBh.loc[ic], PUh.loc[ic]
rows2 = []
for ph in PHASES:
a = win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END)
b = win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END)
a, b = pair(a, b)
rows2.append(dict(ph=ph, corr=float(a.corr(b)), sh_usd=sh(a), sh_usdt=sh(b),
d=sh(a) - sh(b), dd_usd=dd(a), dd_usdt=dd(b)))
R2 = pd.DataFrame(rows2)
print(f" {'fase':>5}{'corr':>9}{'Sh IBRIDO':>11}{'Sh USDT':>10}{'dSharpe':>10}"
f"{'DD IBR':>9}{'DD USDT':>9}")
for _, r in R2.iterrows():
print(f" {int(r['ph']):>5}{r['corr']:>9.4f}{r['sh_usd']:>11.2f}{r['sh_usdt']:>10.2f}"
f"{r['d']:>+10.3f}{r['dd_usd']*100:>8.1f}%{r['dd_usdt']*100:>8.1f}%")
print(f" {'MEDIANA':>5}{R2['corr'].median():>9.4f}{R2['sh_usd'].median():>11.2f}"
f"{R2['sh_usdt'].median():>10.2f}{R2['d'].median():>+10.3f}"
f"{R2['dd_usd'].median()*100:>8.1f}%{R2['dd_usdt'].median()*100:>8.1f}%")
print(f" corr minima {R2['corr'].min():.4f} -> "
f"{'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'} (soglia {CORR_SOGLIA})")
print(f" 📌 IL NUMERO DI TESTA DI §21, RICALCOLATO IN DOLLARI: mediana di fase fuori")
print(f" campione {R2['sh_usdt'].median():+.2f} (USDT, replica) -> "
f"{R2['sh_usd'].median():+.2f} (IBRIDO USD).")
# differenza appaiata con la macchineria del progetto (stesso gate di §21)
dlt = A.anchor_luck_delta(
lambda ph: win(alive(X.xsec_engine(PUh, phase=ph)), T2_START, T2_END),
lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END),
PHASES, metric=sh)
print(f" anchor_luck_delta (IBRIDO - USDT): mediana appaiata {dlt['median_paired']:+.3f} "
f"[{dlt['lo']:+.3f},{dlt['hi']:+.3f}], fasi con IBRIDO>USDT "
f"{dlt['n_positive']}/{dlt['n_anchors']}")
print("\n E il ranking, sull'ibrido (la fase con la corr piu' bassa va GUARDATA, non")
print(" riassunta: una corr di 0,97 su una fase o e' un ribaltamento vero o e' un giunto):")
D2 = ranking_scan(PBh, PUh, PHASES, "TEST 2 / U13-IBRIDO")
print("\n Discordanze del GATE binario sull'ibrido (finestra fuori campione):")
nfl = 0
for ph in PHASES:
for a_, b_, nd, bfirst in gate_flips(PBh, PUh, ph):
if a_ >= pd.Timestamp(T2_END, tz="UTC"):
continue
nfl += 1
print(f" fase {ph}: {a_.date()} .. {b_.date()} ({nd}g) — "
f"{'USDT investito / USD FLAT' if bfirst else 'USD investito / USDT FLAT'}")
print(f" totale {nfl} intervalli su 10 fasi (su ~204 ribilanciamenti per fase)")
cg2 = []
for ph in PHASES:
a_, b_ = pair(win(alive(X.xsec_engine(PUh, phase=ph, disp_pct=0)), T2_START, T2_END),
win(alive(X.xsec_engine(PBh, phase=ph, disp_pct=0)), T2_START, T2_END))
cg2.append(float(a_.corr(b_)))
print(f" ATTRIBUZIONE [TEST 2] corr con gate SPENTO: mediana {np.median(cg2):.4f} "
f"minima {np.min(cg2):.4f} (col gate acceso: mediana {R2['corr'].median():.4f}, "
f"minima {R2['corr'].min():.4f})")
# ------------------------------- [5b] le gambe che NESSUN venue USD puo' controllare
print("\n[5b] LE TRE GAMBE NON CONTROLLABILI — quanto del risultato di §21 ci passa?")
print(" BNB e' il token nativo del venue sotto esame e NON esiste in USD nel 2021-23;")
print(" NEAR e INJ hanno un prezzo USD solo dal 2022-09. Se il titolo di §21 vivesse su")
print(" di loro, questo falsificatore non avrebbe potuto vedere niente, e andrebbe detto.")
base_b = A.anchor_luck_band(
lambda ph: win(alive(X.xsec_engine(PBh, phase=ph)), T2_START, T2_END),
PHASES, canonical=0, metric=sh)
print(f" base U13 (Binance, fuori campione, mediana di fase): {base_b['median']:+.2f}")
for a_ in ("BNB", "NEAR", "INJ"):
sub = p13[[c for c in U13 if c != a_]].dropna()
bb = A.anchor_luck_band(
lambda ph, sub=sub: win(alive(X.xsec_engine(sub, phase=ph, min_assets=10)),
T2_START, T2_END),
PHASES, canonical=0, metric=sh)
print(f" senza {a_:<5s}: mediana di fase {bb['median']:+.2f} "
f"({bb['median']-base_b['median']:+.2f}) banda [{bb['lo']:+.2f},{bb['hi']:+.2f}]")
print(" (leave-one-out a 12 gambe: il meccanismo congelato regge, min_assets=10)")
# ---------------------------------------------------- [6] IL CASO PEGGIORE
print("\n[6] IL CASO PEGGIORE — non la media: dove i due venue divergono di piu'?")
print(f" {'anno':>6}" + "".join(f"{k:>12}" for k in
("corr T1", "dSh T1", "corr T2", "dSh T2")))
for y in (2021, 2022, 2023):
a1 = b1 = a2 = b2 = None
cs1, ds1, cs2, ds2 = [], [], [], []
for ph in PHASES:
a1 = win(alive(X.xsec_engine(PU, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
b1 = win(alive(X.xsec_engine(PB, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
if len(a1) > 60 and len(b1) > 60:
a1, b1 = pair(a1, b1)
cs1.append(float(a1.corr(b1))); ds1.append(sh(a1) - sh(b1))
a2 = win(alive(X.xsec_engine(PUh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
b2 = win(alive(X.xsec_engine(PBh, phase=ph)), f"{y}-01-01", f"{y+1}-01-01")
if len(a2) > 60 and len(b2) > 60:
a2, b2 = pair(a2, b2)
cs2.append(float(a2.corr(b2))); ds2.append(sh(a2) - sh(b2))
f1 = f"{np.median(cs1):.4f}" if cs1 else "—"
g1 = f"{np.median(ds1):+.3f}" if ds1 else "—"
f2 = f"{np.median(cs2):.4f}" if cs2 else "—"
g2 = f"{np.median(ds2):+.3f}" if ds2 else "—"
print(f" {y:>6}{f1:>12}{g1:>12}{f2:>12}{g2:>12}")
worst1 = R.loc[R["corr"].idxmin()]
worst2 = R2.loc[R2["corr"].idxmin()]
wd1 = R.loc[R["d"].abs().idxmax()]
wd2 = R2.loc[R2["d"].abs().idxmax()]
print(f" peggior FASE per corr: T1 fase {int(worst1['ph'])} corr {worst1['corr']:.4f} | "
f"T2 fase {int(worst2['ph'])} corr {worst2['corr']:.4f}")
print(f" peggior FASE per dSharpe: T1 fase {int(wd1['ph'])} {wd1['d']:+.3f} | "
f"T2 fase {int(wd2['ph'])} {wd2['d']:+.3f}")
autopsia(PB, PU, int(worst1["ph"]), D1, "TEST 1")
autopsia(PBh, PUh, int(worst2["ph"]), D2, "TEST 2")
st = idio.abs().stack().sort_values(ascending=False)
print(" le 8 divergenze IDIOSINCRATICHE piu' grandi della finestra del TEST 1 (bps):")
for (t, s), v in st.head(8).items():
indep = "DEPEG" if pd.Timestamp(DEPEG[0], tz="UTC") <= t < pd.Timestamp(DEPEG[1], tz="UTC") else ""
print(f" {str(t.date()):<12}{s:<6}{v:8.0f} bps (comune quel giorno "
f"{common.loc[t]:+.0f} bps) {indep}")
print(" -> le peggiori NON cadono nel depeg e sono quasi tutte AVAX nelle settimane subito")
print(" dopo la sua quotazione su Coinbase: book sottile, non contaminazione da")
print(" stablecoin. Due cause diverse e due implicazioni diverse.")
# --------- il caso peggiore su TUTTO il campione, non solo sulla finestra del test
print("\n ⚠️ E ORA LA DOMANDA GIUSTA, allargata: dove divergono di piu' i due venue in")
print(" TUTTO il campione 2021-2026? (cercare il caso peggiore solo dentro la finestra")
print(" che si sta difendendo e' un modo di non trovarlo)")
have = [s for s in U13 if USD_SRC[s] is not None]
Bf = bin_panel(have)
Uf = usd_panel(have).reindex(Bf.index)
bo = Bf.notna() & Uf.notna()
Df = (1e4 * np.log(Uf / Bf)).where(bo)
idf = Df.sub(Df.median(axis=1), axis=0)
worst_day = idf.abs().max(axis=1).sort_values(ascending=False)
print(f" {'giorno':<12}{'|idio| max':>11}{'asset':>7}{'comune':>9} finestra")
for t in worst_day.head(6).index:
s = idf.loc[t].abs().idxmax()
w = "fuori campione" if t < pd.Timestamp(T2_END, tz="UTC") else "scoperta 2024+"
print(f" {str(t.date()):<12}{idf.loc[t, s]:>11.0f}{s:>7}{Df.loc[t].median():>9.0f} {w}")
tw = worst_day.index[0]
print(f" il giorno peggiore in assoluto e' il {tw.date()}: scarto per asset (bps)")
print(" " + " ".join(f"{s}:{idf.loc[tw, s]:.0f}" for s in have
if np.isfinite(idf.loc[tw, s])))
print(" 📌 E' un fatto che va detto per intero: la divergenza idiosincratica PUO'")
print(" esplodere — qui di due ordini di grandezza — ma e' successo in una CASCATA DI")
print(" LIQUIDAZIONI sulle coppie /USDT, non in un depeg, e cade nella finestra di")
print(" SCOPERTA (2024+, dove lo sleeve gira su Hyperliquid), non nel fuori campione.")
print(" Il meccanismo temuto dall'obiezione e' REALE; non e' accaduto nel 2021-2023.")
# il giorno peggiore: dove il P&L dei due sleeve si stacca di piu'
a0 = alive(X.xsec_engine(PU, phase=0)); b0 = alive(X.xsec_engine(PB, phase=0))
a0, b0 = pair(a0, b0)
gap = (a0 - b0)
print(f" scarto GIORNALIERO fra i due sleeve (fase 0): med "
f"{gap.abs().median()*1e4:.1f} bps p99 {gap.abs().quantile(.99)*1e4:.1f} bps "
f"max {gap.abs().max()*1e4:.1f} bps il {gap.abs().idxmax().date()}")
cum = (1 + a0).cumprod().iloc[-1] / (1 + b0).cumprod().iloc[-1] - 1
print(f" divario CUMULATO su tutta la finestra del TEST 1: {cum*100:+.2f}% "
f"(equity USD vs equity USDT)")
# --------------------------------------------------------------- [7] gate
print("\n[7] GATE")
print(f" soglia di venue (dichiarata dall'autore PRIMA, non da me): corr >= {CORR_SOGLIA}")
print(f" TEST 1 U10 congelato : corr mediana {R['corr'].median():.4f}, minima "
f"{R['corr'].min():.4f} -> {'PASS' if R['corr'].min() >= CORR_SOGLIA else 'FAIL'}")
print(f" TEST 2 U13 ibrido : corr mediana {R2['corr'].median():.4f}, minima "
f"{R2['corr'].min():.4f} -> {'PASS' if R2['corr'].min() >= CORR_SOGLIA else 'FAIL'}"
f" (una fase su 10; col gate spento minima {np.min(cg2):.4f} = PASS)")
print(f" ⚠️ il TEST 2 e' MIO, non quello dichiarato: e' piu' severo per costruzione")
print(f" (le gambe cambiano venue in date diverse) e la sua unica fase sotto soglia")
print(f" e' attribuita al gate binario, non al prezzo.")
print(f" riferimento 2024+ di §21 (Binance vs Hyperliquid): 0,9991 / dSharpe -0,025")
su = alive(X.xsec_engine(PUh, phase=0))
mg = A.marginal_vs_tp01(win(su, T2_START, T2_END))
print(f" marginal_vs_tp01 sullo sleeve IBRIDO fuori campione: {mg.get('marginal_verdict')} "
f"(corr {mg.get('corr_full')}, is_hedge {mg.get('is_hedge')}, "
f"insample_edge {mg.get('has_insample_edge')})")
imp = A.implausible_sharpe(win(su, T2_START, T2_END))
print(f" implausible_sharpe (ibrido OOS): {imp['implausible']} Sh {imp.get('sharpe'):+.2f} "
f"perdite su barre attive {imp.get('loss_frac', float('nan'))*100:.1f}%")
print(" causalita': NON RI-GIRATA — il motore e' importato invariato da r0822b_xs01_oos.py,")
print(" che la verifica (prezzi futuri randomizzati -> max|diff| = 0). Cambiare il venue")
print(" cambia i dati, non l'ordine temporale in cui il motore li legge.")
print(" deflated_sharpe: NON PERTINENTE — qui non si SELEZIONA una cella, si confronta due")
print(" volte lo STESSO meccanismo su due fonti di prezzo. Nessun trial nuovo di strategia.")
print(" null de-levering: NON PERTINENTE (nessun claim di 'meno drawdown').")
print(" anchor: girato per costruzione — ogni numero sopra e' su tutte e 10 le fasi.")
# ------------------------------------------------------------ [8] verdetto
print("\n[8] VERDETTO")
print(f" Il TEST DICHIARATO (meccanismo congelato, stesso sottoinsieme, stessa finestra,")
print(f" USD contro USDT) PASSA: corr mediana {R['corr'].median():.4f}, minima "
f"{R['corr'].min():.4f} >= {CORR_SOGLIA}, dSharpe appaiato {R['d'].median():+.3f}.")
print(f" Il titolo di §21 ricalcolato in dollari si muove di {R2['sh_usd'].median()-R2['sh_usdt'].median():+.2f} "
f"({R2['sh_usdt'].median():+.2f} -> {R2['sh_usd'].median():+.2f} di mediana di fase).")
print(f" Nella finestra del depeg la componente idiosincratica sale (med {idm.median():.1f} bps "
f"contro {idio[~dm].abs().stack().median():.1f}) ma resta due ordini di grandezza sotto")
print(f" la vol giornaliera di un alt, e ZERO ribilanciamenti cambiano gambe li' dentro.")
print(f" L'unica corr sotto soglia (TEST 2, fase 2: {R2['corr'].min():.4f}) e' attribuita:")
print(f" col gate di dispersione SPENTO la minima diventa {np.min(cg2):.4f} -> non e' il")
print(f" prezzo, e' la soglia BINARIA che amplifica pochi bps in 10 giorni di flat.")
print( " -> IL RISULTATO XS01-OOS SOPRAVVIVE al controllo di venue.")
print( " Cio' che NON e' stato controllato, e va citato insieme al verdetto: BNB non ha un")
print( " prezzo USD nel 2021-23 (e' il token nativo del venue sotto esame), e toglierlo")
print( " costa -0.75 di mediana di fase. Il test copre 10-12 gambe su 13, non 13.")
print(f" E il caso peggiore di divergenza dell'INTERO campione ({tw.date()}, "
f"{idf.loc[tw].abs().max():.0f} bps su una gamba)")
print( " dimostra che il meccanismo temuto e' reale: semplicemente non e' accaduto nella")
print( " finestra fuori campione. Non e' 'non puo' succedere', e' 'non e' successo li''.")
print(f"\n[fine] {time.time()-t0:.1f}s")
if __name__ == "__main__":
main()

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