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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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)*
|
||||
@@ -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
@@ -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.*
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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).
|
||||
@@ -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
Reference in New Issue
Block a user