diff --git a/.gitignore b/.gitignore index c5d6fc7..17497f9 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,10 @@ Old/**/__pycache__/ # run logs (rigenerabili dagli script) logs/ +# cache di ricerca rigenerabile (serie d'ancora, tabelle di segnale intra-bin SKH01): +# 22MB, ~2.4h a ricostruirla, ma e' DERIVATA dai parquet certificati -> non si versiona. +data/_cache/ + # cache della ricerca trackE (rigenerabile) .cache_trackE_*.npy diff --git a/CLAUDE.md b/CLAUDE.md index 4fde981..d32a2ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -460,12 +460,21 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis (2) **La rendita NON e' `capitale x CAGR`.** Il 24/07 calcolava capitale = target/CAGR (€122k a CAGR 15%), ignorando il rischio di sequenza. Bootstrap a blocchi sui ritorni REALI (SKH01 su **path live**): rendita **perpetua** (= prelievo con P(cap a 20a >= cap iniziale) >= 90%) del book de-luckato ×0.6 = - **5.98% → $497k**; SWR-20a 7.31% → $406k; lente modellata 13.57% → $219k. **Il muro vero per 50 EUR/g - perpetui e' una BANDA $232k-$497k** (path live nudo → path live + de-luck ×0.6; il ×0.6 sopra il path - live rischia di **contare due volte** la degradazione SKH01 → stima centrale ~$300-400k), non €122k. + **5.98% → $497k**; SWR-20a 7.31% → $406k; lente modellata 13.57% → $219k. ~~Il muro vero e' una + BANDA $232k-$497k, stima centrale ~$300-400k.~~ + ⚠️ **AGGIORNATO 2026-07-26 — il ×0.6 era MISURATO troppo severo del 31-34% → il muro scende + del ~45%.** Il fattore d'ancora onesto sul drift e' **×0.89** per il book live (misurato, non a + occhio) e ogni componente del path live e' non-negativa (bullet in fondo). Ricalcolo con la + STESSA macchineria (`r0726_capwall_refresh.py`): **perpetua 6.00% → 10.91%, muro $494.758 → + $272.061** a leva 1.0 (a leva 1.5: $366k → $200k). Il ×0.89 e' un **limite inferiore** del + fattore, quindi il muro vero sta fra la riga ×0.89 e la riga ×1.00 ($233k). **La conclusione + STRUTTURALE non cambia:** $272k restano **~453× il conto di oggi** → i €600 come CAPITALE + restano refutati, come BIGLIETTO (prop) no. Rendita a $600: €0.12/g → **€0.18/g** (centesimi: + il fattore conta per il MURO e le soglie prop, non per il conto attuale). Cio' che NON dipende dalla lente: **la rendita perpetua vale ~45-55% del CAGR**, quindi ogni muro - calcolato come `target/CAGR` sbaglia di un fattore ~2. A $600 il book rende **~€0.11/giorno**. Traiettoria (bootstrap, - de-luck): **0 depositi → 0% dei path in 20 anni**; €500/m mediana 19.6a; €1000/m mediana 14.8a. + calcolato come `target/CAGR` sbaglia di un fattore ~2. Traiettoria (bootstrap, + de-luck): **0 depositi → 0% dei path in 20 anni**; €500/m mediana 19.6a; €1000/m mediana 14.8a + (⚠️ calcolata a ×0.6: col fattore corretto i tempi si accorciano, non ricalcolata). (3) **Diversificare NON crea reddito a pari nozionale — libera BUDGET DI RISCHIO.** Per tier: iso-nozionale 2 sleeve 5.98% → 4 sleeve 6.35% (nulla, il diversificatore a basso CAGR toglie vol e ritorno insieme); a **ISO-RISCHIO** (vol 15%) 7.51%/$395k → **9.25%/$321k (−19% di muro)**. @@ -613,14 +622,19 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis fortuna +0.182 — contro mediana 0.04 / banda [−0.13,+0.30] misurate il 02/07 con implementazione SEPARATA. **L'hold-out onesto di TP01 e' ~+0.05, non 0.31** (il valore del sleeve resta il taglio del DD ~6× vs buy&hold, che non e' ancorato). - (3) **NON fatto e perche':** il book ricalcolato sul **path live** (sintesi naturale: SKH01 live > - modello su entrambi i lati, TP01 live ≈ modello) e' bloccato da una **incompatibilita' di lenti** — - il simulatore SKH01 compone per-trade a nozionale unitario, lo sleeve e' vol-targeted; combinarle - darebbe un numero preciso e falso. **Follow-up dichiarato:** serve la versione vol-targeted del - path live di SKH01 prima di toccare il fattore de-luck. Conseguenza gia' solida: il **×0.6** fu - scelto assumendo che il live degradi, ma su SKH01 e' misurato il contrario e su TP01 e' ~zero → - **il ×0.6 e' probabilmente CONSERVATIVO** (la nota "rischia di contare due volte la degradazione - SKH01" e' confermata, non ipotetica). + (3) ~~**NON fatto e perche':** il book sul path live e' bloccato da una **incompatibilita' di + lenti** — il simulatore SKH01 compone per-trade a nozionale unitario, lo sleeve e' vol-targeted. + Follow-up dichiarato: serve la versione vol-targeted del path live di SKH01.~~ + ✅ **CHIUSO 2026-07-26 — E IL BLOCCO NON ESISTEVA: la premessa era FALSA.** Lo sleeve SKH01 + **non e' vol-targeted** (`backtest_signals(..., leverage=1.0, position_size=1.0)`, nessun + `target_vol` nel sorgente, e `sim_equity(canonical)` riproduce `backtest_signals` a + **max|diff| = 0.0**): le due lenti erano gia' la stessa cosa. Il 20.7% di vol realizzata dello + sleeve — probabilmente cio' che aveva suggerito "vol-target 20%" — e' un **prodotto** della + strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro; **SKH01 e' l'unica + delle 5 a NON essere vol-targeted**, il contrario di quanto si credeva. Vedi bullet dedicato + in fondo e diario `2026-07-26-skh-live-book-deluck.md`. **REGOLA: un blocco dichiarato e' + un'ipotesi, non un fatto — si ri-verifica prima di costruirci sopra o di rimandare** (qui: tre + sessioni di follow-up rimandato contro due minuti di verifica). - **Ondata 2026-07-26 (3 filoni: esecuzione SKH01 / DVOLSPREAD / XSR01 fuori dal crypto) — 0 sleeve nuovi, 1 raccomandazione operativa aperta, 1 lead promosso, 1 falsificazione.** Script `r0726_{skh_onbook,dvolspread_gate,xsr_equity}.py`, test `tests/test_wave_0726.py` (11 casi), @@ -795,6 +809,47 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis arrotondamento di 99.55% con `%.0f`. **Un percentile stampato a 0 decimali mente esattamente agli estremi, che sono l'unico posto dove lo si legge.** Verificato a mano (9 estrazioni su 2000 battono la canonica), formato portato a 1 decimale. +- ✅ **FOLLOW-UP SKH01 "vol-targeted" CHIUSO (2026-07-26, 4° filone) — il blocco non esisteva, e + il fattore de-luck ×0.6 era troppo severo del 31-34%.** Script `r0726_skh_sigcache.py` (cache), + `r0726_skh_live_book.py` (misura), `r0726_deluck_factor.py` (decomposizione), + `r0726_capwall_refresh.py` (muri); test `tests/test_skh_live_book.py` (8); diario + `2026-07-26-skh-live-book-deluck.md`. **Book/pesi/cron/config INVARIATI.** + (1) **La premessa era falsa** — vedi il bullet dell'ondata 26/07-bis: SKH01 **non e' + vol-targeted**, le due lenti erano gia' identiche (`sim_equity(canonical)` == `backtest_signals` + a max|diff| = 0.0). Test permanente `test_lente_sleeve_uguale_a_quella_del_simulatore`: se + qualcuno aggiunge un vol-target allo sleeve, la premessa torna vera e **il test deve rompersi** + invece di far passare numeri non piu' validi. + (2) **Ingressi live vs backtest, de-luckato a 8 offset** (sottocampione uniforme a priori: la + griglia piena a 23 costa ~9 min per asset×offset, fuori portata su 2 core col cron live). + Sanity **120/120** bin con ingresso ricostruiti. **Il numero del 26/07 era ~4× troppo grande:** + like-with-like (mediana PER-ASSET) **+0.38 su 3 offset → +0.097 su 8**, e "6/6 non negativi" → + **13/16**. A livello di sleeve 50/50 la mediana appaiata e' **+0.112, positiva 8/8**. + ⚠️ Le due grandezze (per-asset e sleeve 50/50) **non sono la stessa cosa** — nel book entra la + seconda, dove la diversificazione BTC/ETH cambia il denominatore. + (3) **A livello di BOOK: Sharpe ≈ monetina, drift certo.** dSh FULL +0.048 (>0 nel 79.3%), + dSh HOLD +0.051 (51.2%), **d drift +0.73pp (>0 nel 100.0%)**. Meccanismo: l'ingresso intra-bin + prende un prezzo migliore ma i falsi ingressi aggiungono churn (530-605 trade live vs 411-484) + → vol e ritorno salgono insieme. **REGOLA: chiedersi QUALE grandezza entra nella decisione + prima di misurare** — qui il ×0.6 e' sul drift, quindi lo Sharpe non era la metrica. + (4) **Il ×0.6 decomposto e misurato.** (a) fortuna d'ancora **sul drift**: **×0.874** (5-sleeve) + / **×0.890** (book live) — e la **vol e' invariata** fra le ancore (7.80→7.76%), la fortuna sta + tutta nel drift; (b) path live: **non-negativo ovunque** (uscite +0.081 Sh, ingressi +0.73pp, + TP01 trascurabile). Il ×0.6 implicava un residuo **×0.687** attribuito al live oltre l'ancora, + che nessuna misura sostiene → **FATTORE ONESTO ×0.87-0.91 (5-sleeve), ≥×0.89 (book live)**. + Il sospetto registrato il 25/07 ("conta due volte la degradazione SKH01") e' **confermato + quantitativamente**. **REGOLA: un fattore correttivo scelto a occhio va DECOMPOSTO prima di + accettarlo o rifiutarlo** — il ×0.6 non era sbagliato, era due fattori moltiplicati insieme di + cui uno contato due volte. + (5) ❌ **Ipotesi MIA nata e refutata nella stessa sessione:** che la grid timing-luck di SKH01 + (audit 02/07) fosse un artefatto della lente a chiusura-di-bin, perche' il live valuta ogni ora + e non e' ancorato al confine del bin. Dispersione fra gli 8 offset: **LIVE/CANONICO = 1.59×**, + cioe' il live e' **piu'** disperso, non meno → **l'audit del 02/07 resta valido com'e'**. + L'ipotesi nasceva da **2** offset, si e' incrinata a 4 (1.24×), chiusa a 8. **REGOLA: due punti + non fanno una tendenza nemmeno quando la meccanica sembra spiegarla.** + (6) ⚠️ **Trovato per strada:** `simulate()` in `r0726_skh_partial_entry.py` **non e' mai chiamata + da `main()`** — i numeri headline del diario del 26/07 vennero da una corsa ad hoc mai + committata. Ora il driver e' committato (`r0726_skh_live_book.py`). *Un numero pubblicato in un + diario deve avere uno script committato che lo riproduce.* - **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55. - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.** diff --git a/docs/diary/2026-07-26-skh-live-book-deluck.md b/docs/diary/2026-07-26-skh-live-book-deluck.md new file mode 100644 index 0000000..4a94b6b --- /dev/null +++ b/docs/diary/2026-07-26-skh-live-book-deluck.md @@ -0,0 +1,189 @@ +# 2026-07-26 — Follow-up SKH01 "vol-targeted": il blocco non esisteva. E il ×0.6 era troppo severo del 31-34%. + +**Richiesta:** *"fai il follow-up SKH01 vol-targeted"*. + +**Script:** `r0726_skh_sigcache.py` (cache), `r0726_skh_live_book.py` (misura), +`r0726_deluck_factor.py` (decomposizione), `r0726_capwall_refresh.py` (muri ricalcolati). +**Test:** `tests/test_skh_live_book.py` (8). **Book, pesi, cron, config: INVARIATI.** + +--- + +## 0. Il follow-up era mal diagnosticato + +Portato avanti come bloccante per tre sessioni, con questa motivazione registrata in CLAUDE.md: + +> il book ricalcolato sul path live e' bloccato da una **incompatibilita' di lenti** — il +> simulatore SKH01 compone per-trade a nozionale unitario, **lo sleeve e' vol-targeted**; +> combinarle darebbe un numero preciso e falso. + +**Lo sleeve SKH01 non e' vol-targeted.** Tre verifiche indipendenti: + +1. `_skyhook_returns` chiama `backtest_signals(..., leverage=1.0, position_size=1.0)` — che nel + suo docstring dice *"ogni trade muove capital di position_size \* leverage \* ret_netto"*: + compounding per-trade a nozionale pieno, **la stessa lente del simulatore**; +2. nel sorgente dello sleeve non compare `target_vol` / `vol_target` / `realized_vol`; +3. `r0702_anchor_skh01.sim_equity(mode='canonical')` riproduce `backtest_signals` con + **max|diff| = 0.0**. + +Vol annualizzata realizzata per sleeve: SKH01 **20.7%**, TP01 12.2%, XS01 20.9%, GTAA01 5.4%. +Il 20.7% e' probabilmente cio' che aveva suggerito "vol-target 20%" — ma e' un **prodotto** della +strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro. **SKH01 e' l'unica +delle cinque a NON essere vol-targeted**, ed e' il contrario di quanto si credeva. + +**Lezione:** un blocco dichiarato va ri-verificato prima di costruirci sopra, non ereditato. Qui +il costo e' stato tre sessioni di follow-up rimandato per un ostacolo inesistente, e la verifica +che lo ha smontato e' durata due minuti. + +--- + +## 1. La misura: ingressi live vs backtest, de-luckata + +Due path identici in tutto (livelli, uscite intra-barra, cap `max_per_day`, fee, non-overlap) +tranne **quando si valuta l'ingresso**: LIVE = a ogni confine orario dentro il bin 230m (cio' che +il cron fa), BACKTEST = solo a chiusura di bin. + +Riusa la macchina a stati fedele del 26/07 (`r0726_skh_partial_entry.simulate`). ⚠️ Trovato per +strada: **`simulate` non e' mai chiamata da `main()` di quello script** — i numeri headline del suo +diario vennero da una corsa ad hoc mai committata. Qui il driver e' committato. + +**Costo:** la ricostruzione del segnale a ogni osservazione oraria costa ~9 min per (asset, +offset). Su 2 core con il cron live da non affamare, la griglia piena (23 offset) era fuori +portata → **sottocampione uniforme dichiarato a priori: 8 offset**, uno ogni 90m su [0,690). +E' un sottocampione per costo, non una selezione; la banda che ne esce ha 8 punti invece di 23. + +**Sanity superato prima di tutto** (campionato sugli EVENTI, non sulla popolazione — lezione del +26/07): **120/120** bin con ingresso ricostruiti su entrambi gli asset. + +``` + off CANON ShBT ShLIVE dSharpe trade LIVE trade BT falsi + 0 +1.446 +1.174 +1.204 +0.029 534 447 68 + 90 +1.043 +1.182 +1.194 +0.011 538 472 67 + 180 +1.467 +1.505 +1.698 +0.193 544 469 60 + 270 +1.231 +1.199 +1.205 +0.006 605 477 179 + 360 +0.761 +1.069 +1.397 +0.328 570 478 109 + 450 +0.921 +0.917 +2.025 +1.108 530 484 36 + 540 +0.751 +0.374 +0.445 +0.071 531 411 138 + 630 +0.871 +0.991 +1.144 +0.154 460 412 47 + +SLEEVE (50/50) mediana appaiata +0.112 positive 8/8 banda [+0.006, +1.108] +PER-ASSET mediana appaiata +0.097 positive 13/16 banda [-0.170, +1.086] +``` + +**Il numero del 26/07 era ~4x troppo grande.** Confrontando like-with-like — quel diario citava la +mediana **per-asset** — si passa da **+0.38 su 3 offset** a **+0.097 su 8**, e da "6/6 non +negativi" a **13/16**. Il segno regge, la taglia no: ennesimo Δ misurato su poche ancore che ne +eredita la fortuna. L'outlier a offset 450 (+1.108) mostra perche' la statistica e' la mediana. + +⚠️ Le due righe della tabella **non sono la stessa grandezza** e vanno tenute separate: nel BOOK +entra lo sleeve 50/50, dove la diversificazione BTC/ETH cambia il denominatore. + +### A livello di book + +``` +dSharpe FULL mediana +0.048 p10 -0.015 p90 +0.677 >0 in 79.3% +dSharpe HOLD-OUT mediana +0.051 p10 -0.589 p90 +0.721 >0 in 51.2% +d drift annuo mediana +0.731pp p10 +0.044pp p90 +6.800pp >0 in 100.0% +``` + +Lo Sharpe e' quasi una monetina, **il drift e' positivo nel 100% delle estrazioni**. Meccanismo: +l'ingresso intra-bin prende un prezzo migliore (piu' drift) ma i falsi ingressi aggiungono churn +(530-605 trade live contro 411-484) → vol e ritorno salgono insieme e lo Sharpe non si muove. +Per la domanda del ×0.6, che e' **sul drift**, il numero rilevante e' +0.73pp. + +--- + +## 2. Un'ipotesi mia, nata e morta nella stessa sessione + +Guardando i primi **due** offset avevo notato che il canonico oscillava di 0.40 mentre i path del +simulatore stavano fermi, e ne avevo tratto un'ipotesi: la *grid timing-luck* di SKH01 (audit +02/07: 93-98° pctl, "spike non plateau", gate DD<30% che fallisce in 15/23 offset) sarebbe stata +in buona parte un **artefatto della lente a chiusura-di-bin**, non una fragilita' della strategia +live — che valuta ogni ora e non e' ancorata al confine del bin. + +**Refutata.** Dispersione dello Sharpe fra gli 8 offset: + +``` +path min mediana max range std +CANONICO (chiusura bin) +0.751 +0.982 +1.467 0.716 0.289 +BT-sim (chiusura bin) +0.374 +1.122 +1.505 1.131 0.325 +LIVE-sim (intra-bin) +0.445 +1.204 +2.025 1.580 0.459 + +rapporto di dispersione LIVE/CANONICO: 1.59x +``` + +Il path live e' **piu'** disperso fra le fasi della griglia, non meno. **L'audit del 02/07 resta +valido com'e':** la timing-luck di SKH01 e' della strategia, non della lente. L'ipotesi e' nata su +2 offset, si e' incrinata a 4 (1.24x) e si e' chiusa a 8 (1.59x) — e la direzione dell'errore era +sempre la stessa, il che e' il motivo per cui 2 punti non bastano mai. + +--- + +## 3. Il ×0.6 decomposto, misurato, corretto + +Il fattore taglia il **drift** (`b - 0.4*mean(b)`) ed era applicato **sopra** la lente `hourly`, +cioe' sopra un modello del live gia' pessimistico. Mescolava due cose separabili. + +**(a) Fortuna d'ancora sul drift** — misurata sullo spazio congiunto: + +| | canonico | pctl | MEDIANA | p10-p90 | +|---|---|---|---|---| +| book 5-sleeve, drift | 17.33% | 96.0° | **15.15%** | [13.88, 16.49] | +| book 5-sleeve, CAGR | 18.56% | 96.0° | **16.00%** | [14.55, 17.56] | +| book LIVE 75/25, drift | 19.27% | 93.8° | **17.16%** | [15.75, 18.67] | + +→ fattore d'ancora **×0.874** (5-sleeve) / **×0.890** (book live). La **vol e' invariata** fra le +ancore (7.80% → 7.76%): la fortuna sta tutta nel drift, non nel rischio. + +**(b) Degradazione del path live** — non-negativa su tutto cio' che e' stato misurato: +uscite SKH01 **+0.081** Sharpe di book (23/23 offset); ingressi SKH01 **+0.73pp** di drift +(100% delle estrazioni); TP01 barra parziale trascurabile (24 ancore). + +**Il ×0.6 implicava un residuo ×0.687 attribuito al live oltre l'ancora.** Nessuna misura del +progetto lo sostiene. Il sospetto gia' registrato in CLAUDE.md — *"il ×0.6 sopra il path live +rischia di contare DUE VOLTE la degradazione SKH01"* — e' **confermato quantitativamente**. + +> **FATTORE ONESTO: ×0.87 – ×0.91** (5-sleeve) / **≥×0.89** (book live), contro il ×0.60 in uso. +> **Troppo severo del 31-34%.** + +La componente uscite spingerebbe oltre, ma e' misurata in Sharpe e non in drift: non convertita, +per non inventare precisione. Quindi il limite superiore e' esso stesso conservativo. + +--- + +## 4. Cosa cambia nei muri di capitale (`r0726_capwall_refresh.py`) + +Stessa macchineria del 25/07 (`book_series`, `survival`, `_boot_paths` importati, non riscritti). + +``` +fattore leva SWR-20a PERPETUA capitale +×0.60 (25/07, a occhio) 1.00 7.36% 6.00% $494,758 +×0.60 (25/07, a occhio) 1.50 8.16% 8.11% $366,300 +×0.89 (26/07, ancora MISURATA) 1.00 10.94% 10.91% $272,061 +×0.89 (26/07, ancora MISURATA) 1.50 13.72% 14.87% $199,625 +×1.00 (nessun haircut) 1.00 12.48% 12.72% $233,330 +``` + +**Il muro per 50 EUR/g scende del ~45%: da ~$495k a ~$272k** (leva 1.0), e il valore vero sta fra +la riga ×0.89 e la riga ×1.00 perche' ×0.89 e' un limite inferiore. + +**Cio' che NON cambia:** $272k restano **~453x** il conto di oggi. La conclusione strutturale del +25/07 — *i 600 euro come CAPITALE sono refutati, come BIGLIETTO (prop) no* — regge intatta. +Cambia la taglia dell'errore, e cambia che il numero e' misurato invece che scelto. + +Rendita sul conto attuale: EUR 0.12/g (×0.6) → **EUR 0.18/g** (×0.89). A questa taglia sono +centesimi: **il fattore conta per il MURO e per le soglie prop, non per il conto di oggi.** + +--- + +## 5. Regole trasferibili + +1. **Un blocco dichiarato e' un'ipotesi, non un fatto.** Ri-verificarlo prima di costruirci sopra + o di rimandare: qui tre sessioni di attesa contro due minuti di verifica. +2. **Un Δ su poche ancore eredita la loro fortuna, sempre** — terza occorrenza in due giorni + (recupero on-book, degrado d'esecuzione, e ora gli ingressi: +0.38 → +0.097). +3. **Quando un fattore correttivo e' scelto a occhio, decomporlo prima di rifiutarlo o accettarlo.** + Il ×0.6 non era "sbagliato": era due fattori moltiplicati insieme, uno dei quali contato due volte. +4. **Sharpe e drift rispondono a cose diverse.** L'effetto ingressi e' una monetina sullo Sharpe e + certo sul drift: chiedersi *quale* delle due grandezze entra nella decisione prima di misurare. +5. **Due punti non fanno una tendenza** — nemmeno quando la meccanica sembra spiegarla. L'ipotesi + sulla dispersione era plausibile e sbagliata, e il costo di verificarla era basso. diff --git a/scripts/research/r0726_capwall_refresh.py b/scripts/research/r0726_capwall_refresh.py new file mode 100644 index 0000000..2ed18b6 --- /dev/null +++ b/scripts/research/r0726_capwall_refresh.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +"""r0726_capwall_refresh.py — i MURI DI CAPITALE ricalcolati col fattore de-luck corretto. + +Il 25/07 (`r0725_capcurve.py`) ha calcolato rendita perpetua, muro di capitale per 50 EUR/g e +traiettoria da $600 applicando un de-luck **×0.6 scelto a occhio** sul drift del book. + +Il 26/07 quel fattore e' stato MISURATO e decomposto (`r0726_deluck_factor.py` + +`r0726_skh_live_book.py`): + * fortuna d'ancora sul drift, book live Deribit (TP01/SKH01 75/25): **×0.890** + (24 x 23 = 552 configurazioni, estrazioni congiunte); + * degradazione del path live: **non-negativa su tutto cio' che e' misurato** + (SKH01 uscite +0.081 Sharpe di book su 23/23 offset; SKH01 ingressi +0.73pp di drift, + positivo nel 100% delle estrazioni; TP01 barra parziale trascurabile su 24 ancore). +Il ×0.6 implicava quindi un residuo ×0.687 attribuito al live OLTRE l'ancora, che nessuna misura +sostiene: era il doppio conteggio gia' sospettato in CLAUDE.md, ora quantificato. + +Questo script rifa' i numeri della DECISIONE ai tre fattori, sulla stessa macchineria del 25/07 +(nessuna re-implementazione: `book_series`, `survival`, `_boot_paths` importati da li'). + + uv run python scripts/research/r0726_capwall_refresh.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")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0725_capcurve as CC # noqa: E402 + +# fattori a confronto. 0.890 = fortuna d'ancora MISURATA sul book live Deribit; il vero fattore +# onesto e' >= 0.890 perche' la componente live e' non-negativa (non la conto: e' conservativo). +FACTORS = ( + (0.60, "×0.60 (25/07, a occhio)"), + (0.89, "×0.89 (26/07, ancora MISURATA)"), + (1.00, "×1.00 (nessun haircut)"), +) +LEVERAGES = (1.0, 1.5) +N_PATHS = 2000 + + +def perp_and_wall(r: np.ndarray, k: float, gross_needed: float) -> tuple[float, float, float]: + """(SWR-20a, tasso di rendita PERPETUA, capitale necessario) — stessa ricerca binaria + del 25/07: perpetua = prelievo con P(cap a 20a >= cap iniziale) >= 90%.""" + lo, hi = 0.0, 0.40 + for _ in range(13): + mid = (lo + hi) / 2 + s = CC.survival(r, 1e6, 1e6 * mid, 20, k, n_paths=N_PATHS) + if s["p10_end"] >= 1e6: + lo = mid + else: + hi = mid + perp = lo + lo2, hi2 = 0.0, 0.40 + for _ in range(13): + mid = (lo2 + hi2) / 2 + if CC.survival(r, 1e6, 1e6 * mid, 20, k, n_paths=N_PATHS)["p_survive"] >= 0.95: + lo2 = mid + else: + hi2 = mid + cap = gross_needed / perp if perp > 0.002 else float("inf") + return lo2, perp, cap + + +def main() -> None: + print("=" * 100) + print(" r0726 — MURI DI CAPITALE col fattore de-luck MISURATO (era ×0.6 a occhio)") + print("=" * 100) + + gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) + print(f" bersaglio: {CC.TARGET_EUR_DAY:.0f} EUR/g netti = EUR " + f"{CC.TARGET_EUR_DAY*365:,.0f}/anno netti = ${gross:,.0f}/anno LORDI " + f"(fisco {CC.TAX_RATE:.0%}, EURUSD {CC.EURUSD})") + print(" book: TP01+SKH01 75/25 su path live (lente 'hourly'), costi reali Deribit\n") + + base = CC.book_series("hourly") + mu = float(base.values.mean()) + + print(f" {'fattore':<32}{'leva':>6}{'SWR-20a':>10}{'PERPETUA':>10}{'capitale':>16}") + print(" " + "-" * 74) + res = {} + for f, lab in FACTORS: + r = (base - (1.0 - f) * mu).values.astype(float) + for k in LEVERAGES: + swr, perp, cap = perp_and_wall(r, k, gross) + res[(f, k)] = (swr, perp, cap) + capf = f"${cap:,.0f}" if np.isfinite(cap) else "IRRAGGIUNGIBILE" + print(f" {lab:<32}{k:>6.2f}{swr:>9.2%}{perp:>10.2%}{capf:>16}") + print() + + print("=" * 100) + print(" COSA CAMBIA NELLA DECISIONE") + print("=" * 100) + for k in LEVERAGES: + c_old = res[(0.60, k)][2] + c_new = res[(0.89, k)][2] + if np.isfinite(c_old) and np.isfinite(c_new): + print(f" leva {k:.2f}: muro ${c_old:,.0f} -> ${c_new:,.0f} " + f"({(c_new/c_old - 1)*100:+.0f}%)") + c_new = res[(0.89, 1.0)][2] + print(f"\n Il muro scende di ~45%, ma resta ${c_new:,.0f} = ~{c_new/600:,.0f}x il conto") + print(" di oggi ($600). La conclusione STRUTTURALE del 25/07 non cambia: i 600 euro") + print(" come CAPITALE restano refutati, come BIGLIETTO (prop) no. Cambia la taglia") + print(" dell'errore — e cambia che il numero ora e' misurato invece che scelto.") + print("\n ⚠️ Il ×0.89 e' la sola fortuna d'ancora: e' un LIMITE INFERIORE del fattore") + print(" onesto, perche' ogni componente misurata del path live e' non-negativa.") + print(" Il muro vero sta quindi fra la riga ×0.89 e la riga ×1.00.") + + # rendita a $600 — il numero che il 25/07 dava a ~0.11 EUR/g + print("\n" + "=" * 100) + print(" RENDITA A $600 (lo stesso conto di oggi)") + print("=" * 100) + for f, lab in FACTORS: + r = (base - (1.0 - f) * mu).values.astype(float) + cagr = float((1 + r.mean()) ** 365.25 - 1) + eur = 600.0 * cagr * (1 - CC.TAX_RATE) / CC.EURUSD / 365.0 + print(f" {lab:<32} CAGR {cagr*100:6.2f}% rendita EUR {eur:.2f}/giorno") + print("\n (a questa taglia la differenza fra ×0.6 e ×0.89 sono centesimi al giorno:") + print(" il fattore conta per il MURO e per le soglie prop, non per il conto attuale.)") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0726_deluck_factor.py b/scripts/research/r0726_deluck_factor.py new file mode 100644 index 0000000..af8330e --- /dev/null +++ b/scripts/research/r0726_deluck_factor.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +"""r0726_deluck_factor.py — DECOMPOSIZIONE del fattore de-luck ×0.6 del 25/07. + +IL PROBLEMA. `r0725_capcurve.py` applica `b = b - 0.4*mean(b)` ("de-luck ×0.6": taglia il 40% del +DRIFT, lascia la vol) sopra un book gia' calcolato con SKH01 sulla lente `hourly`. Il ×0.6 fu +scelto **a occhio**, e CLAUDE.md registrava gia' il sospetto: *"il ×0.6 sopra il path live rischia +di contare DUE VOLTE la degradazione SKH01"*. + +Il fattore mescola due cose che ora si possono separare e misurare: + (a) **fortuna d'ancora** — i numeri canonici sono un punto alto dello spazio d'ancora + (misurato il 26/07 sul Sharpe: FULL 97.0 pctl, HOLD 99.6 pctl). Qui serve sul **DRIFT**, + perche' e' il drift che il ×0.6 taglia. + (b) **degradazione del path live** — che per SKH01 e' misurata **NEGATIVA** (il live e' MEGLIO + del backtest su entrambi i lati: uscite +0.081 di Sharpe FULL di book su 23/23 offset, + ingressi +0.38 di Sharpe di sleeve) e per TP01 e' ~zero (±0.03 su 24 ancore). + +Questo script misura (a) e la riporta accanto a (b), per due book: + * il book ATTIVO 5-sleeve (33/15/12/20/20) + * il book LIVE Deribit (TP01/SKH01 75/25) — quello che gira davvero + +METODO: identico a `r0726_loo_deluck.py` (stesse serie d'ancora, stesse estrazioni congiunte +uniformi indipendenti), ma la metrica e' il **drift annualizzato** (media giornaliera x 365.25) e +il CAGR, non il Sharpe. Il fattore onesto e' `mediana / canonico` sul drift. + +NON decide da solo il valore da usare: la componente (b) sugli INGRESSI di SKH01 e' in corso di +misura de-luckata (`r0726_skh_sigcache.py` + `r0726_skh_live_book.py`). Qui si stabilisce quanto +del ×0.6 e' spiegato dall'ancora — cioe' quanto ne resta da attribuire al live. + + uv run python scripts/research/r0726_deluck_factor.py [--draws N] +""" +from __future__ import annotations + +import argparse +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")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0726_loo_deluck as L # noqa: E402 +from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics # noqa: E402 + +DPY = 365.25 + +BOOKS = { + "book ATTIVO 5-sleeve (33/15/12/20/20)": L.WEIGHTS, + "book LIVE Deribit (TP01/SKH01 75/25)": {"TP01_trend_1d": 0.75, "SKH01_skyhook": 0.25}, +} + + +def drift_and_cagr(cols: dict, weights: dict) -> tuple[float, float, float]: + """(drift annualizzato, CAGR, vol annualizzata) del book a quei pesi.""" + w = {k: weights[k] for k in cols} + s = combine_outer(cols, w) + r = np.asarray(s.dropna().values, float) + m = metrics(s) + return float(r.mean() * DPY), float(m["cagr"]), float(r.std() * np.sqrt(DPY)) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=2000) + args = ap.parse_args() + + print("=" * 100) + print(" r0726 — DECOMPOSIZIONE del fattore de-luck ×0.6 (25/07)") + print("=" * 100) + print(" Il ×0.6 taglia il DRIFT del book (b - 0.4*mean(b)). Quanto di quel taglio e'") + print(" spiegato dalla FORTUNA D'ANCORA, e quanto resterebbe da attribuire al path live?") + + S = L.build_all() + rng = np.random.default_rng(L.SEED) + cfgs = [{n: int(rng.choice(L.SPEC[n][1])) for n in L.SPEC} for _ in range(args.draws)] + canon_cfg = {n: L.SPEC[n][2] for n in L.SPEC} + + for label, W in BOOKS.items(): + names = list(W) + print("\n" + "=" * 100) + print(f" {label}") + print("=" * 100) + + def cols_at(cfg): + return {n: S[n][cfg[n]] for n in names} + + d_can, c_can, v_can = drift_and_cagr(cols_at(canon_cfg), W) + rows = np.array([drift_and_cagr(cols_at(c), W) for c in cfgs], float) + d_med, c_med, v_med = np.median(rows, axis=0) + d_lo, d_hi = np.percentile(rows[:, 0], [10, 90]) + c_lo, c_hi = np.percentile(rows[:, 1], [10, 90]) + + n_anch = int(np.prod([len(L.SPEC[n][1]) for n in names])) + print(f" spazio d'ancora di questo book: {n_anch:,} configurazioni " + f"({' x '.join(str(len(L.SPEC[n][1])) for n in names)})") + print(f"\n {'':<24}{'canonico':>11}{'pctl':>8}{'MEDIANA':>11}{'p10':>10}{'p90':>10}") + print(f" {'drift annualizzato':<24}{d_can*100:10.2f}%" + f"{float((rows[:, 0] < d_can).mean()*100):7.1f}%{d_med*100:10.2f}%" + f"{d_lo*100:9.2f}%{d_hi*100:9.2f}%") + print(f" {'CAGR':<24}{c_can*100:10.2f}%" + f"{float((rows[:, 1] < c_can).mean()*100):7.1f}%{c_med*100:10.2f}%" + f"{c_lo*100:9.2f}%{c_hi*100:9.2f}%") + print(f" {'vol annualizzata':<24}{v_can*100:10.2f}%{'':>8}{v_med*100:10.2f}%") + + f_drift = d_med / d_can if d_can else float("nan") + f_cagr = c_med / c_can if c_can else float("nan") + print(f"\n >>> FATTORE D'ANCORA sul drift : ×{f_drift:.3f} (il ×0.6 ne assume ×0.600)") + print(f" >>> FATTORE D'ANCORA sul CAGR : ×{f_cagr:.3f}") + residuo = 0.6 / f_drift if f_drift else float("nan") + print(f" >>> residuo implicito da attribuire al PATH LIVE: ×{residuo:.3f}") + if residuo < 1.0: + print(f" cioe' il ×0.6 assume che il live tolga un altro " + f"{(1-residuo)*100:.0f}% di drift OLTRE l'ancora.") + else: + print(f" cioe' il ×0.6 e' CONSERVATIVO anche prima di contare il live.") + + print("\n" + "=" * 100) + print(" COMPONENTE (b) — degradazione del path live, gia' misurata") + print("=" * 100) + print(" SKH01 uscite : +0.081 Sharpe FULL di book, 23/23 offset (26/07, banda appaiata)") + print(" SKH01 ingressi: +0.38 Sharpe di sleeve, mediana su 3 offset x 2 asset (26/07)") + print(" -> in corso la versione de-luckata a 8 offset, a livello di BOOK") + print(" TP01 barra parziale: -0.031 FULL / +0.118 HOLD su 24 ancore = trascurabile") + print(" => il segno di (b) e' NON-NEGATIVO su tutto cio' che e' stato misurato.") + print(" Un fattore <1 per il live non e' sostenuto da nessuna misura del progetto.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0726_skh_live_book.py b/scripts/research/r0726_skh_live_book.py new file mode 100644 index 0000000..5d6684a --- /dev/null +++ b/scripts/research/r0726_skh_live_book.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +"""r0726_skh_live_book.py — IL BOOK SUL PATH LIVE DI SKH01, de-luckato. Chiude il follow-up. + +FOLLOW-UP CHE CHIUDE (portato avanti bloccato dal 26/07, registrato in CLAUDE.md): + +> il book ricalcolato sul path live e' bloccato da una **incompatibilita' di lenti** — il +> simulatore SKH01 compone per-trade a nozionale unitario, lo sleeve e' vol-targeted; combinarle +> darebbe un numero preciso e falso. Serve la versione vol-targeted del path live di SKH01. + +⚠️ **LA PREMESSA ERA FALSA e il blocco non esisteva.** Verificato in tre modi: + 1. `sleeves._skyhook_returns` chiama `backtest_signals(..., leverage=1.0, position_size=1.0)`: + **nessun vol-targeting**, compounding per-trade a nozionale pieno — la STESSA lente del + simulatore. Il 20.7% di vol realizzata dello sleeve e' un prodotto della strategia (uscite + % asimmetriche + poco tempo a mercato), non un target; + 2. `grep` di `target_vol|vol_target|realized_vol` nel sorgente dello sleeve: nessuna occorrenza; + 3. `r0702_anchor_skh01.sim_equity(mode='canonical')` riproduce `backtest_signals` con + **max|diff| = 0.0** — le due lenti sono gia' la stessa cosa. +Il follow-up non era bloccato: era mal diagnosticato. Questo script fa la misura. + +COSA MISURA. Due path identici in tutto (livelli, uscite intra-barra, cap `max_per_day`, fee, +non-overlap) tranne **quando si valuta l'ingresso**: + * LIVE = a ogni confine orario dentro il bin 230m (cio' che il cron fa davvero); + * BACKTEST = solo alla chiusura del bin (cio' che il backtest assume). +Riusa la macchina a stati fedele gia' scritta e testata il 26/07 (`r0726_skh_partial_entry +.simulate`) — che pero' `main()` di quello script **non chiamava mai**: i numeri headline del suo +diario (3 offset) vennero da una corsa ad hoc mai committata. Qui il driver e' committato. + +DE-LUCK. Il 26/07 la misura era su **3 offset**; la lezione dello stesso giorno dice che *se si +de-lucka una strategia va de-luckato anche il suo DEGRADO*. Qui: **8 offset** (sottocampione +uniforme a priori, `r0726_skh_sigcache.OFFSETS_SUB`) x 2 asset, e il salto a livello di BOOK con +gli altri 4 sleeve estratti sulle loro ancore -> **mediana delle differenze appaiate**. + +USO: la cache dei segnali va costruita prima (~9 min per asset x offset): + uv run python scripts/research/r0726_skh_sigcache.py + uv run python scripts/research/r0726_skh_live_book.py [--draws N] +""" +from __future__ import annotations + +import argparse +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")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + +import r0702_anchor_skh01 as r02 # noqa: E402 +import r0726_loo_deluck as L # noqa: E402 +import r0726_skh_partial_entry as SP # noqa: E402 +import r0726_skh_sigcache as SC # noqa: E402 +from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics, to_daily # noqa: E402 + +ASSETS = ("BTC", "ETH") +SKH = "SKH01_skyhook" +DPY = 365.25 + + +def _sh(s: pd.Series) -> float: + r = np.asarray(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 paths_for(asset: str, off: int) -> dict: + """(daily LIVE, daily BACKTEST, diagnostica) per un asset a un offset.""" + tab = SC.get_signal_table(asset, off, verbose=False) + _, _, ltf, _ = r02.run_asset(asset, off) + df5 = r02.get5m(asset) + ts5 = df5["timestamp"].values.astype(np.int64) + h5 = df5["high"].values.astype(float) + l5 = df5["low"].values.astype(float) + c5 = df5["close"].values.astype(float) + live = SP.simulate(tab, ltf, ts5, h5, l5, c5, intra_entry=True) + bt = SP.simulate(tab, ltf, ts5, h5, l5, c5, intra_entry=False) + return dict(live=live["daily"], bt=bt["daily"], + n_live=live["n_trades"], n_bt=bt["n_trades"], n_false=live["n_false"]) + + +def skh_book(dailies: dict) -> pd.Series: + """50/50 BTC+ETH, convenzione di _skyhook_returns (inner join sulla griglia giornaliera).""" + J = pd.concat(dailies, 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) + + +def book_metrics(cols: dict, weights: dict) -> tuple[float, float, float]: + w = {k: weights[k] for k in cols} + s = combine_outer(cols, w) + m = metrics(s) + r = np.asarray(s.dropna().values, float) + return m["sharpe"], metrics(s[s.index >= HOLDOUT])["sharpe"], float(r.mean() * DPY) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=200, + help="estrazioni delle ancore degli ALTRI 4 sleeve, per offset SKH") + args = ap.parse_args() + + have = [o for o in SC.OFFSETS_SUB + if all(SC.cache_path(a, o).exists() for a in ASSETS)] + if len(have) < 3: + print(f"[STOP] servono almeno 3 offset in cache, presenti {len(have)}: {have}") + print(" gira prima: uv run python scripts/research/r0726_skh_sigcache.py") + sys.exit(1) + + print("=" * 100) + print(" r0726 — IL BOOK SUL PATH LIVE DI SKH01 (de-luckato). Chiude il follow-up.") + print("=" * 100) + print(f" offset in cache: {have} ({len(have)}/{len(SC.OFFSETS_SUB)} del sottocampione " + f"a priori, {len(SC.OFFSETS_SUB)}/23 della griglia piena)") + + # ---------------- 0. SANITY: la ricostruzione a fine bin == backtest -------------- + print("\n" + "-" * 100) + print(" 0. SANITY — a chiusura di bin la ricostruzione deve dare il segnale del backtest") + print(" (campionata sugli EVENTI: gli ingressi sono ~2% dei bin, a caso il check e' vacuo)") + print("-" * 100) + for a in ASSETS: + r = SP.selfcheck(a, off=0) + ok = r["sig_ok"] == r["sig_tot"] + print(f" {a}: bin CON ingresso {r['sig_ok']}/{r['sig_tot']} " + f"{'OK' if ok else '*** DIVERGE ***'} | flat {r['flat_ok']}/{r['flat_tot']} " + f"(+{r['flat_capped']} con segnale soppresso da max_per_day: ATTESO, non divergenza)") + if not ok: + sys.exit(1) + + # ---------------- 1. livello SLEEVE, per offset ----------------------------------- + print("\n" + "-" * 100) + print(" 1. SLEEVE SKH01 — Sharpe del path LIVE vs BACKTEST, per offset") + print("-" * 100) + print(f" {'off':>5}{'CANON':>9}{'ShBT':>9}{'ShLIVE':>9}{'dSharpe':>10}" + f"{'trade LIVE':>12}{'trade BT':>10}{'falsi':>8}") + P = {} + rows = [] + can_v, bt_v, live_v, per_asset = [], [], [], [] + for off in have: + d = {a: paths_for(a, off) for a in ASSETS} + live = skh_book({a: d[a]["live"] for a in ASSETS}) + bt = skh_book({a: d[a]["bt"] for a in ASSETS}) + P[off] = (live, bt) + s_c, s_l, s_b = _sh(r02.skh_port(off)), _sh(live), _sh(bt) + rows.append(s_l - s_b) + can_v.append(s_c); bt_v.append(s_b); live_v.append(s_l) + for a in ASSETS: # per-asset: la grandezza citata il 26/07 + per_asset.append((off, a, _sh(d[a]["live"]) - _sh(d[a]["bt"]))) + print(f" {off:>5}{s_c:+9.3f}{s_b:+9.3f}{s_l:+9.3f}{s_l - s_b:+10.3f}" + f"{sum(d[a]['n_live'] for a in ASSETS):>12}" + f"{sum(d[a]['n_bt'] for a in ASSETS):>10}" + f"{sum(d[a]['n_false'] for a in ASSETS):>8}") + arr = np.array(rows, float) + print(f"\n SLEEVE (50/50) mediana delle differenze APPAIATE: {np.median(arr):+.3f} " + f"positive {int((arr > 0).sum())}/{len(arr)} banda [{arr.min():+.3f}, {arr.max():+.3f}]") + pa = np.array([x[2] for x in per_asset], float) + print(f" PER-ASSET (la grandezza citata il 26/07) : {np.median(pa):+.3f} " + f"positive {int((pa > 0).sum())}/{len(pa)} banda [{pa.min():+.3f}, {pa.max():+.3f}]") + print(" ⚠️ le due righe NON sono la stessa cosa: il 26/07 cito' la mediana dei Δ") + print(" PER-ASSET (+0.38 su 3 offset x 2 asset). Il numero che entra nel BOOK e'") + print(" quello dello SLEEVE 50/50, dove la diversificazione BTC/ETH cambia il") + print(" denominatore. Vanno confrontati like-with-like.") + + # ---- 1b. DISPERSIONE fra offset: chi e' davvero esposto alla fortuna di griglia? ---- + print("\n" + "-" * 100) + print(" 1b. DISPERSIONE FRA OFFSET — la timing-luck di griglia di SKH01 e' del BACKTEST") + print(" o della STRATEGIA? (il live non e' ancorato al confine di bin: valuta ogni ora)") + print("-" * 100) + print(f" {'path':<28}{'min':>9}{'mediana':>10}{'max':>9}{'range':>9}{'std':>9}") + for lab, v in (("CANONICO (chiusura bin)", can_v), ("BT-sim (chiusura bin)", bt_v), + ("LIVE-sim (intra-bin)", live_v)): + v = np.array(v, float) + print(f" {lab:<28}{v.min():+9.3f}{np.median(v):+10.3f}{v.max():+9.3f}" + f"{v.max()-v.min():9.3f}{v.std(ddof=1):9.3f}") + rc = np.array(can_v, float); rl = np.array(live_v, float) + if rc.std(ddof=1) > 0: + ratio = rl.std(ddof=1) / rc.std(ddof=1) + print(f"\n rapporto di dispersione LIVE/CANONICO: {ratio:.2f}x") + if ratio < 1.0: + print(" <1 -> il path live e' MENO sensibile alla fase della griglia: parte della") + print(" 'grid timing-luck' del 02/07 sarebbe un artefatto della lente a") + print(" chiusura-di-bin, non una fragilita' della strategia live.") + else: + print(" >=1 -> IPOTESI REFUTATA. Il path live e' PIU' disperso fra le fasi della") + print(" griglia, non meno: la grid timing-luck di SKH01 (audit 02/07) NON e'") + print(" un artefatto della lente a chiusura-di-bin. L'audit resta valido") + print(" com'e'. (Ipotesi nata su 2 offset, refutata a 4, confermata a 8.)") + + # ---------------- 2. livello BOOK, de-luckato ------------------------------------- + print("\n" + "-" * 100) + print(" 2. BOOK 5-sleeve — effetto degli INGRESSI live, con gli altri 4 sleeve estratti") + print("-" * 100) + S = L.build_all() + others = [n for n in L.SPEC if n != SKH] + rng = np.random.default_rng(L.SEED) + d_full, d_hold, d_drift = [], [], [] + for off in have: + live, bt = P[off] + for _ in range(args.draws): + cfg = {n: int(rng.choice(L.SPEC[n][1])) for n in others} + base = {n: S[n][cfg[n]] for n in others} + fl, hl, dl = book_metrics({**base, SKH: to_daily(live)}, L.WEIGHTS) + fb, hb, db = book_metrics({**base, SKH: to_daily(bt)}, L.WEIGHTS) + d_full.append(fl - fb) + d_hold.append(hl - hb) + d_drift.append(dl - db) + for lab, v in (("dSharpe FULL", d_full), ("dSharpe HOLD-OUT", d_hold), + ("d drift annuo", d_drift)): + v = np.array(v, float) + k = 100.0 if "drift" in lab else 1.0 + u = "pp" if "drift" in lab else "" + print(f" {lab:<20} mediana {np.median(v)*k:+7.3f}{u} " + f"p10 {np.percentile(v, 10)*k:+7.3f}{u} p90 {np.percentile(v, 90)*k:+7.3f}{u} " + f">0 in {(v > 0).mean()*100:5.1f}%") + + # ---------------- 3. il fattore de-luck ------------------------------------------ + print("\n" + "=" * 100) + print(" 3. IL FATTORE DE-LUCK ×0.6 — decomposto e ricomposto") + print("=" * 100) + dd = np.median(np.array(d_drift, float)) + print(" (a) fortuna d'ancora sul drift del book 5-sleeve : ×0.874 (r0726_deluck_factor)") + print(f" (b) path live SKH01, componente INGRESSI : {dd*100:+.3f}pp di drift di book") + print(" (b) path live SKH01, componente USCITE : +0.081 Sharpe FULL (23/23 off.)") + print(" (b) path live TP01, barra parziale : trascurabile (24 ancore)") + drift_can = 0.1733 # drift canonico del book 5-sleeve (r0726_deluck_factor) + f_lo = 0.874 # ancora sola: ignora il live (conservativo) + f_hi = 0.874 * (1.0 + dd / drift_can) # ancora + componente INGRESSI misurata qui + print(f"\n Il ×0.6 implica ×0.687 di residuo attribuito al live OLTRE l'ancora.") + print(" Nessuna misura del progetto sostiene un residuo <1: tutte le componenti") + print(" misurate del path live sono NON-NEGATIVE.") + print(f"\n >>> FATTORE ONESTO: ×{f_lo:.2f} - ×{f_hi:.2f} (contro il ×0.60 in uso)") + print(f" ×{f_lo:.2f} = sola fortuna d'ancora, ignorando il live (conservativo)") + print(f" ×{f_hi:.2f} = ancora + componente INGRESSI ({dd*100:+.2f}pp su {drift_can*100:.2f}%)") + print(" La componente USCITE (+0.081 Sharpe) spingerebbe oltre, ma e' misurata in") + print(" Sharpe e non in drift: non la converto per non inventare precisione.") + print(f" => il ×0.6 e' troppo severo di circa il {(1 - 0.6/f_lo)*100:.0f}-{(1 - 0.6/f_hi)*100:.0f}%.") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/r0726_skh_sigcache.py b/scripts/research/r0726_skh_sigcache.py new file mode 100644 index 0000000..778018a --- /dev/null +++ b/scripts/research/r0726_skh_sigcache.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +"""r0726_skh_sigcache.py — costruisce e mette in CACHE le tabelle di segnale intra-bin di SKH01. + +E' il pezzo caro del follow-up "book sul path live" (`r0726_skh_live_book.py`): per ogni +(asset, offset) ricostruisce il segnale che il cron ORARIO vedrebbe a OGNI osservazione dentro +ogni bin 230m — ~82.000 righe, ~9 minuti. Isolato qui perche' e' (a) puro, (b) riusabile, e +(c) ripartibile: la cache e' su disco, il job si puo' interrompere e riprendere. + +Sottocampione di offset dichiarato A PRIORI: 8 degli 23 della griglia SKH01, uno ogni 90 minuti +su [0, 690). NON e' una selezione — e' un sottocampione uniforme scelto per costo (2 core, ~9 +min per run, cron live da non affamare). Va dichiarato perche' la banda d'ancora che ne esce ha +8 punti invece di 23. + + uv run python scripts/research/r0726_skh_sigcache.py [--assets BTC ETH] [--offsets ...] +""" +from __future__ import annotations + +import argparse +import sys +import time +from pathlib import Path + +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 r0726_skh_partial_entry as SP # noqa: E402 + +# 8 offset uniformi su [0,690), uno ogni 90m. Dichiarati a priori, offset 0 = canonico. +OFFSETS_SUB = (0, 90, 180, 270, 360, 450, 540, 630) +ASSETS = ("BTC", "ETH") +CACHE_DIR = ROOT / "data" / "_cache" / "skh_sigtab" + + +def cache_path(asset: str, off: int) -> Path: + return CACHE_DIR / f"sigtab_{asset.lower()}_off{off:03d}.parquet" + + +def get_signal_table(asset: str, off: int, verbose: bool = True) -> pd.DataFrame: + """Tabella di segnale intra-bin, da cache se presente.""" + p = cache_path(asset, off) + if p.exists(): + return pd.read_parquet(p) + t0 = time.time() + tab = SP.signal_table(asset, off) + p.parent.mkdir(parents=True, exist_ok=True) + tab.to_parquet(p, index=False) + if verbose: + print(f" {asset} off{off:>3} {len(tab):>7} righe " + f"segnali {int((tab['dir'] != 0).sum()):>5} in {time.time()-t0:5.0f}s", flush=True) + return tab + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--assets", nargs="+", default=list(ASSETS)) + ap.add_argument("--offsets", nargs="+", type=int, default=list(OFFSETS_SUB)) + args = ap.parse_args() + + print(f"[SIGCACHE] {len(args.assets)} asset x {len(args.offsets)} offset " + f"-> {CACHE_DIR}", flush=True) + t0 = time.time() + done = 0 + for off in args.offsets: # offset esterno: cosi' ogni offset e' + for a in args.assets: # completo su entrambi gli asset appena finisce + p = cache_path(a, off) + if p.exists(): + print(f" {a} off{off:>3} gia' in cache", flush=True) + continue + get_signal_table(a, off) + done += 1 + print(f"[SIGCACHE] fatto: {done} nuove tabelle in {time.time()-t0:.0f}s", flush=True) + + +if __name__ == "__main__": + main() diff --git a/tests/test_skh_live_book.py b/tests/test_skh_live_book.py new file mode 100644 index 0000000..c2d708b --- /dev/null +++ b/tests/test_skh_live_book.py @@ -0,0 +1,119 @@ +"""Test per il follow-up "book sul path live di SKH01" (r0726_skh_live_book / _deluck_factor). + +Il test piu' importante e' `test_lente_sleeve_uguale_a_quella_del_simulatore`: e' l'invariante su +cui poggia TUTTA la misura, ed e' esattamente la premessa che per tre sessioni si era creduta +FALSA ("lo sleeve e' vol-targeted, il simulatore no"). Se qualcuno domani aggiunge un vol-target +a `_skyhook_returns`, quella premessa diventa vera e i numeri di questo filone smettono di valere: +il test deve fallire, non passare in silenzio. +""" +from __future__ import annotations + +import inspect +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) + + +# =========================================================================== +# L'INVARIANTE DI LENTE — la premessa del follow-up +# =========================================================================== +def test_sleeve_skyhook_non_e_vol_targeted(): + """`_skyhook_returns` compone per-trade a nozionale unitario: niente vol-target. + E' il fatto che sblocca il follow-up; se cambia, va rifatta tutta la misura.""" + from src.portfolio.sleeves import _skyhook_returns + + src = inspect.getsource(_skyhook_returns) + assert "leverage=1.0" in src + for kw in ("target_vol", "vol_target", "realized_vol"): + assert kw not in src, f"lo sleeve ora usa {kw}: la lente e' cambiata, rifare r0726" + + +def test_lente_sleeve_uguale_a_quella_del_simulatore(): + """sim_equity(mode='canonical') deve riprodurre backtest_signals ESATTAMENTE. + E' cio' che rende confrontabili path live e sleeve senza conversioni.""" + import r0702_anchor_skh01 as r02 + + _, m, ltf, ent = r02.run_asset("BTC", 0) + eq = r02.sim_equity(ltf, ent, "canonical") + assert float(np.max(np.abs(eq - m.equity))) == 0.0 + + +# =========================================================================== +# Convenzioni di combinazione +# =========================================================================== +def _s(vals, start="2020-01-01"): + return pd.Series(np.asarray(vals, float), + index=pd.date_range(start, periods=len(vals), freq="1D", tz="UTC")) + + +def test_skh_book_e_5050_su_inner_join(): + import r0726_skh_live_book as LB + + a = _s([0.01, 0.02, 0.03]) + b = _s([0.03, 0.02, 0.01]) + out = LB.skh_book({"BTC": a, "ETH": b}) + assert list(np.round(out.values, 10)) == [0.02, 0.02, 0.02] + + +def test_skh_book_inner_join_taglia_le_date_non_comuni(): + """Convenzione dello sleeve: inner join. Se un asset ha meno storia, il book parte dopo.""" + import r0726_skh_live_book as LB + + a = _s([0.01] * 5, start="2020-01-01") + b = _s([0.03] * 3, start="2020-01-03") + out = LB.skh_book({"BTC": a, "ETH": b}) + assert len(out) == 3 + assert out.index[0] == pd.Timestamp("2020-01-03", tz="UTC") + + +def test_book_metrics_drift_e_annualizzato_a_365(): + import r0726_skh_live_book as LB + + r = 0.001 + cols = {"X": _s([r] * 400)} + _, _, drift = LB.book_metrics(cols, {"X": 1.0}) + assert drift == pytest.approx(r * 365.25, rel=1e-9) + + +def test_drift_and_cagr_coerenti(): + import r0726_deluck_factor as DF + + r = 0.0005 + cols = {"X": _s([r] * 800)} + drift, cagr, vol = DF.drift_and_cagr(cols, {"X": 1.0}) + assert drift == pytest.approx(r * 365.25, rel=1e-9) + assert cagr == pytest.approx((1 + r) ** 365.25 - 1, rel=1e-6) # serie costante -> CAGR esatto + assert vol == pytest.approx(0.0, abs=1e-12) # nessuna varianza + + +# =========================================================================== +# Il sottocampione di offset: dichiarato a priori, non selezionato +# =========================================================================== +def test_offsets_sub_e_un_sottoinsieme_uniforme_della_griglia_piena(): + import r0702_anchor_skh01 as r02 + import r0726_skh_sigcache as SC + + full = set(r02.OFFSETS) + assert set(SC.OFFSETS_SUB) <= full, "il sottocampione contiene offset fuori griglia" + assert SC.OFFSETS_SUB[0] == 0, "l'offset canonico deve essere incluso" + passi = np.diff(np.array(SC.OFFSETS_SUB)) + assert len(set(passi.tolist())) == 1, f"sottocampione non uniforme: passi {passi}" + + +def test_cache_path_e_deterministico_e_separa_asset_e_offset(): + import r0726_skh_sigcache as SC + + p1 = SC.cache_path("BTC", 0) + p2 = SC.cache_path("BTC", 90) + p3 = SC.cache_path("ETH", 0) + assert p1 != p2 and p1 != p3 + assert p1 == SC.cache_path("BTC", 0) + assert p1.suffix == ".parquet"