diff --git a/CLAUDE.md b/CLAUDE.md index c64241c..b9bd6e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -989,6 +989,41 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis **l'outer-join con referenze di lunghezza diversa e' una trappola ricorrente**); (f) un controllo positivo finito dentro il ramo `else` **non gira mai** (trovato in sessione: era esattamente il difetto che doveva prevenire). +- ⚖️ **RIVALUTAZIONE DELLA STRATEGIA (2026-07-26, fine giornata) — 0 cambi, e il peso 75/25 + confermato per la TERZA volta.** Script `r0726_reeval_live_weight.py`, test + `tests/test_reeval_live_weight.py` (6), diario `2026-07-26-reeval-strategia.md`. + **Book, pesi, cron, config INVARIATI.** + (1) **Solo UNA delle sei misure del giorno apriva una decisione**, e per una ragione precisa: il + peso 75/25 fu confermato il 24/07 con la lente **hourly**, che il 26/07 e' risultata pessimistica + su ENTRAMBI i lati di SKH01 (uscite +0.081, ingressi +0.048 di Sharpe di book) → *se SKH01 vale + piu' di come e' stato pesato, 0.25 e' ancora giusto?* Contro tirava il LOO de-luckato dello stesso + giorno (SKH01 = il meno affidabile dei cinque). **Due correzioni in versi opposti: si misura, non + si deduce.** + (2) **Misura** (path live `intra_entry=True`, 8 offset, mediana delle differenze appaiate): + argmax a **w=0.35** e 0.30-0.40 batte 0.25 nell'**88%** degli offset — il segnale c'e' ed e' nel + verso previsto. Ma **plateau entro 0.05 = [0.25...0.50]** (il peso live e' dentro) e + **`weights_tilt_null` FALLISCE** (delta_insample −0.0026, gate_pass False). + (3) ⚠️ **Il motivo vero sta in cio' che la mediana nasconde: il guadagno e' tutto nella coda + ALTA.** p10 per peso: 1.463 (0.25) / 1.465 (0.30) / 1.455 (0.35) / 1.435 (0.40) / 1.374 (0.50) + mentre il p90 sale monotono 1.915→2.064. **Alzare SKH01 non compra Sharpe, compra dipendenza da + quale ancora ti e' capitata** (banda da 0.45 a 0.69). Coerente con altre due misure indipendenti: + SKH01 ha la frazione d'ancora piu' grande da restituire (LOO) ed e' **4x** piu' fee-sensibile + (curva fee). **Tre misure indipendenti: SKH01 e' la gamba fragile, e 0.25 sta all'estremo prudente + della regione robusta.** + (4) 📌 **La rivalutazione che conta non e' sulla strategia.** Ordini di grandezza a confronto: + ottimizzare il peso = **+0.030 Sharpe** (gate fallito); versare **€250/mese invece di €0** = da + **mai** a **16.2 anni** (P(entro 20a) 92%); perdere il conto Deribit a p=1% = **−18%** di + probabilita' di arrivare, ripartendo da zero. **Il book e' dentro il suo plateau su ogni asse + misurato: la ricerca ha smesso di essere il vincolo binding.** I vincoli binding oggi sono + **capitale che entra** e **conto che non sparisce**. (Non significa che la ricerca sia finita — + significa che il margine residuo vale centesimi di Sharpe contro leve che valgono il risultato.) + **REGOLE:** (a) una rivalutazione parte dall'elenco di cosa **cambia una decisione**, non dal + riassunto di cosa si e' scoperto; (b) quando due correzioni puntano in versi opposti **si misura** + (a occhio si poteva argomentare qualunque cosa); (c) **un argmax dentro un plateau non e' una + decisione**, e la mediana da sola puo' nascondere il fatto decisivo (qui: p90 su, p10 giu'); + (d) quando le leve hanno ordini di grandezza diversi **dirlo**, o si lavora molto senza cambiare + niente; (e) rivalutare **non** vuol dire anticipare i gate pre-registrati — anticiparli e' + selezione sull'hold-out. - 📅 **NUOVO SCHEMA FEE DERIBIT dal 2026-08-01 — misurata la CURVA, nessuna azione oggi (2026-07-26).** Script `r0726_fee_sensitivity.py`, test `tests/test_fee_sensitivity.py` (6), diario `2026-07-26-fee-deribit.md`. **Book/pesi/cron/config INVARIATI.** diff --git a/docs/diary/2026-07-26-reeval-strategia.md b/docs/diary/2026-07-26-reeval-strategia.md new file mode 100644 index 0000000..75779da --- /dev/null +++ b/docs/diary/2026-07-26-reeval-strategia.md @@ -0,0 +1,113 @@ +# 2026-07-26 — Rivalutazione della strategia dopo le correzioni del giorno + +**Richiesta:** *"in base a quanto definito rivalutiamo la strategia"*. + +**Script:** `r0726_reeval_live_weight.py`. **Test:** `tests/test_reeval_live_weight.py` (6). +**Book, pesi, cron, config: INVARIATI.** + +--- + +## 0. Cosa c'era da rivalutare davvero + +La giornata ha prodotto sei misure che toccano la strategia. Solo **una** apriva una decisione: + +| misura di oggi | cambia una decisione? | +|---|---| +| fattore de-luck ×0.6 → ×0.89 | no: cambia i *numeri* (muro −45%), non cosa si fa | +| muro come punto fisso ($273.900) | no: la mia previsione era sbagliata, il piano resta | +| LOO de-luckato (SKH01 il meno affidabile) | **forse**: e' un argomento per *ridurre* SKH01 | +| path live SKH01 migliore su entrambi i lati | **forse**: argomento opposto, per *aumentarlo* | +| curva fee (SKH01 4x piu' sensibile) | no oggi: regola gia' decisa per il 1 agosto | +| rischio venue + tripwire | no: decisione dell'operatore gia' registrata | + +Le due righe "forse" puntano in **versi opposti** e nessuna delle due si puo' dedurre a tavolino. +Il peso 75/25 fu confermato il **24/07** con la lente *hourly* — la stessa che il 26/07 e' risultata +pessimistica su entrambi i lati di SKH01 (uscite **+0.081** Sharpe FULL di book, ingressi **+0.048**). +Quindi la domanda e' legittima: **se SKH01 vale piu' di come e' stato pesato, 0.25 e' ancora giusto?** + +## 1. La misura + +Path live vero (`intra_entry=True`), 8 offset del sottocampione a priori, statistica = mediana +delle differenze **appaiate** per offset. + +``` + peso SKH Sh FULL med banda p10-p90 Sh HOLD med d vs 0.25 (appaiata) + 0.00 1.289 [1.289, 1.289] 0.296 -0.322 ( 12%) + 0.15 1.523 [1.425, 1.714] 0.705 -0.091 ( 12%) + 0.20 1.575 [1.450, 1.825] 0.825 -0.038 ( 12%) + 0.25 1.611 [1.463, 1.915] 0.928 +0.000 ( 0%) <- LIVE + 0.30 1.632 [1.465, 1.982] 1.012 +0.022 ( 88%) + 0.35 1.639 [1.455, 2.028] 1.078 +0.030 ( 88%) + 0.40 1.634 [1.435, 2.055] 1.127 +0.025 ( 88%) + 0.50 1.595 [1.374, 2.064] 1.188 -0.013 ( 38%) +``` + +**Argmax a w=0.35**, e in **88% degli offset** 0.30-0.40 batte 0.25: il segnale c'e' ed e' nel verso +previsto dalla correzione di lente. Ma: + +* **plateau entro 0.05 di Sharpe = [0.25, 0.30, 0.35, 0.40, 0.50]** — il peso live e' dentro, quindi + la differenza col massimo non e' distinguibile; +* **`weights_tilt_null` FALLISCE** (`delta_insample −0.0026`, `gate_pass False`); +* e soprattutto la lettura che la mediana da sola nasconde ⬇. + +## 2. Il motivo vero per non toccarlo: il guadagno e' tutto nella coda ALTA + +``` +peso p10 mediana p90 +0.25 1.463 1.611 1.915 +0.30 1.465 1.632 1.982 +0.35 1.455 1.639 2.028 +0.40 1.435 1.634 2.055 +0.50 1.374 1.595 2.064 +``` + +Il **p10 e' piatto fino a 0.30 e poi SCENDE**, mentre il p90 sale monotono. Alzare il peso di SKH01 +non compra Sharpe: compra **dipendenza da quale ancora ti e' capitata**. La banda si allarga da +0.45 (a w=0.25) a 0.69 (a w=0.50). + +E' coerente con il leave-one-out de-luckato dello stesso giorno — SKH01 e' lo sleeve con la +frazione d'ancora piu' grande da restituire — e con la curva fee, dove SKH01 e' **4x** piu' +sensibile di TP01. Tre misure indipendenti dicono la stessa cosa: **SKH01 e' la gamba fragile del +book, e 0.25 sta all'estremo prudente della regione robusta.** + +**→ Nessun cambio. Il 75/25 e' confermato per la terza volta** (24/07 lente hourly, 26/07 path live +intra-bin, e ora il gate sui pesi). + +## 3. La rivalutazione che conta non e' sulla strategia + +Messi in fila, gli ordini di grandezza di oggi: + +| leva | effetto misurato | +|---|---| +| ottimizzare il peso SKH01 (0.25 → 0.35) | **+0.030 Sharpe**, gate fallito | +| versare €250/mese invece di €0 | da **mai** a **16.2 anni** (mediana), P(entro 20a) 92% | +| perdere il conto Deribit (p=1%, 20 anni) | **−18%** di probabilita' di arrivare, e si riparte da zero | + +**L'esito del piano non e' piu' governato dalla strategia.** Il book e' dentro il suo plateau su +ogni asse misurato; il margine residuo di ricerca vale centesimi di Sharpe, mentre i versamenti +valgono la differenza fra "mai" e "16 anni" e il rischio di venue vale il 18% del risultato. + +Questo **non** dice che la ricerca sia finita — dice che la ricerca ha smesso di essere il vincolo +binding. Il vincolo binding oggi e' **capitale che entra** e **conto che non sparisce**. + +## 4. Cio' che la rivalutazione NON cambia + +* **La strategia resta quella**: TP01 0.75 / SKH01 0.25 su Deribit, cron orario, cap 1.0x. +* **I gate pre-registrati restano alle loro date** (1 ago fee, 27 set STATARB, 23 ott XSR01, + 24 ott kill DVOLSPREAD, 24 gen 2027 decisione DVOLSPREAD). Rivalutare non vuol dire anticiparli: + anticipare un gate e' selezione sull'hold-out. +* **La decisione venue resta $20k**, con il tripwire a coprire la parte di rischio che ha una + finestra. + +## 5. Regole trasferibili + +1. **Una rivalutazione onesta parte dall'elenco di cosa CAMBIA una decisione**, non dal riassunto + di cosa si e' scoperto. Sei misure, una sola apriva qualcosa. +2. **Quando due correzioni puntano in versi opposti, si misura**: qui l'una (lente pessimistica) + spingeva ad alzare SKH01, l'altra (LOO de-luckato) ad abbassarlo, e a occhio si sarebbe potuto + argomentare qualunque cosa. +3. **Un argmax dentro un plateau non e' una decisione.** E la mediana da sola nascondeva il fatto + decisivo: il guadagno stava tutto nel p90, il p10 peggiorava. +4. **Quando le leve hanno ordini di grandezza diversi, dirlo.** +0.030 di Sharpe e "da mai a 16 + anni" non sono la stessa categoria di scelta, e trattarle come tali e' il modo piu' comune di + lavorare molto senza cambiare niente. diff --git a/scripts/research/r0726_reeval_live_weight.py b/scripts/research/r0726_reeval_live_weight.py new file mode 100644 index 0000000..bf1ed43 --- /dev/null +++ b/scripts/research/r0726_reeval_live_weight.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +"""r0726_reeval_live_weight.py — il peso 75/25 regge dopo le correzioni di oggi? + +PERCHE' RIAPRIRLO. Il peso del book live (TP01 0.75 / SKH01 0.25) fu confermato il **24/07** +(`r0724_skh_live_weight.py`) misurando il path live con la lente **hourly**. Il 26/07 quella lente +e' risultata **sistematicamente pessimistica su ENTRAMBI i lati** di SKH01: + * USCITE: `resample_5m` non scarta il bin in corso e `_skyhook_positions` ci itera dentro -> il + live rileva SL/TP intra-barra; la lente hourly sottostima **+0.081 Sharpe FULL di book**; + * INGRESSI: il cron valuta a ogni confine orario dentro il bin 230m, non a chiusura di bin -> + mediana appaiata **+0.112** a livello di sleeve, **+0.048** di Sharpe FULL di book. +Se SKH01 vale piu' di come e' stato pesato, **l'ottimo potrebbe non essere piu' 0.25**. E in senso +opposto tira il leave-one-out de-luckato dello stesso giorno, che lo da' come il **meno affidabile** +dei cinque sleeve (+0.142 FULL, positivo nel 91% delle estrazioni, contro +0.390/100% di TP01). + +Due correzioni misurate che puntano in versi OPPOSTI: la domanda non e' retorica, e la risposta non +si puo' dedurre. Qui si misura. + +METODO + * path live vero: `intra_entry=True` (ingressi intra-bin) sui segnali gia' in cache; + * griglia di peso 0.00-0.50, su 8 offset (sottocampione a priori `OFFSETS_SUB`); + * statistica = **mediana delle differenze APPAIATE** per offset (mai differenza di mediane); + * qualunque cambio proposto passa poi da `weights_tilt_null`, che e' il gate del progetto. + + uv run python scripts/research/r0726_reeval_live_weight.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0726_skh_live_book as LB # noqa: E402 +import r0726_skh_sigcache as SC # noqa: E402 +from src.portfolio.portfolio import (combine_outer, metrics, # noqa: E402 + to_daily, weights_tilt_null) +from src.portfolio.sleeves import tp01_sleeve # noqa: E402 + +W_GRID = (0.00, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.50) +W_LIVE = 0.25 +HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") + + +def _sh(s: pd.Series) -> float: + v = s.dropna() + return float(v.mean() / v.std() * np.sqrt(365)) if len(v) > 2 and v.std() > 0 else 0.0 + + +def main() -> None: + print("=" * 100) + print(" r0726 — RIVALUTAZIONE del peso live TP01/SKH01 dopo le correzioni del 26/07") + print("=" * 100) + print(" Il 24/07 confermo' 0.25 con la lente HOURLY. Oggi quella lente e' misurata") + print(" pessimistica su entrambi i lati di SKH01 (uscite +0.081, ingressi +0.048 di book).") + print(" Contro: il LOO de-luckato da' SKH01 come il MENO affidabile dei cinque.") + + have = [o for o in SC.OFFSETS_SUB + if (SC.CACHE_DIR / f"sigtab_btc_off{o:03d}.parquet").exists() + and (SC.CACHE_DIR / f"sigtab_eth_off{o:03d}.parquet").exists()] + print(f"\n offset disponibili in cache: {have} ({len(have)}/23 della griglia piena)") + if len(have) < 4: + print(" ✋ troppo pochi offset: rilanciare r0726_skh_sigcache.py") + return + + tp = tp01_sleeve(weight=1.0).daily() + + # ---- per ogni offset, il path LIVE di SKH01 e le curve di Sharpe al variare del peso + rows_full, rows_hold = {w: [] for w in W_GRID}, {w: [] for w in W_GRID} + for off in have: + dailies = {} + for a in ("BTC", "ETH"): + dailies[a] = LB.paths_for(a, off)["live"] + skh = LB.skh_book(dailies) + for w in W_GRID: + bk = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 1.0 - w, "SKH01": w}) + rows_full[w].append(_sh(bk)) + rows_hold[w].append(_sh(bk[bk.index >= HOLDOUT])) + + print(f"\n {'peso SKH':>9}{'Sh FULL med':>13}{'banda p10-p90':>22}" + f"{'Sh HOLD med':>13}{'d vs 0.25 (appaiata)':>23}") + base_f = np.array(rows_full[W_LIVE]) + base_h = np.array(rows_hold[W_LIVE]) + best_f, best_w = -9e9, None + for w in W_GRID: + f = np.array(rows_full[w]) + h = np.array(rows_hold[w]) + dpair = np.median(f - base_f) # mediana delle DIFFERENZE, non differenza di mediane + mf = float(np.median(f)) + if mf > best_f: + best_f, best_w = mf, w + tag = " <- LIVE" if w == W_LIVE else "" + print(f" {w:>9.2f}{mf:>13.3f}" + f" [{np.percentile(f,10):>6.3f},{np.percentile(f,90):>6.3f}]" + f"{float(np.median(h)):>13.3f}{dpair:>+15.3f}" + f" ({(f>base_f).mean()*100:>3.0f}%){tag}") + + print(f"\n argmax della mediana FULL: w = {best_w:.2f} (live = {W_LIVE:.2f})") + + # ---- plateau: un argmax senza plateau e' rumore + near = [w for w in W_GRID if np.median(rows_full[w]) >= best_f - 0.05] + print(f" plateau entro 0.05 di Sharpe: {near}") + if W_LIVE in near: + print(f" -> il peso live E' dentro il plateau: la differenza col massimo non e'") + print(f" distinguibile, e cambiare peso su questa base sarebbe rumore.") + else: + print(f" -> il peso live e' FUORI dal plateau: il cambio va valutato dal gate.") + + # ---- gate del progetto su un eventuale cambio + if best_w != W_LIVE: + print(f"\n GATE `weights_tilt_null` sul cambio {W_LIVE:.2f} -> {best_w:.2f}:") + off0 = have[0] + skh0 = LB.skh_book({a: LB.paths_for(a, off0)["live"] for a in ("BTC", "ETH")}) + res = weights_tilt_null({"TP01": to_daily(tp), "SKH01": to_daily(skh0)}, + {"TP01": 1 - W_LIVE, "SKH01": W_LIVE}, + {"TP01": 1 - best_w, "SKH01": best_w}, + k_seen=len(W_GRID)) + for k in ("delta_insample", "pctl_hold", "gate_pass"): + if k in res: + print(f" {k:<16} {res[k]}") + print(f"\n ⚠️ Anche a gate passato servirebbero finestre OOS disgiunte e un controllo di") + print(f" eseguibilita' al peso aumentato: il gate e' NECESSARIO, non sufficiente.") + else: + print(f"\n Nessun cambio da valutare: l'argmax coincide col peso live.") + + # ---- sensibilita' alla fee, che il 1 agosto puo' cambiare + print(f"\n PROMEMORIA fee (r0726_fee_sensitivity): SKH01 e' ~4x piu' sensibile di TP01") + print(f" (-0.69% vs -0.09% di CAGR per bps/lato). La regola gia' decisa resta:") + print(f" taker <=5bps -> non si tocca nulla; >10bps -> rivedere PRIMA il peso di SKH01.") + + +if __name__ == "__main__": + main() diff --git a/tests/test_reeval_live_weight.py b/tests/test_reeval_live_weight.py new file mode 100644 index 0000000..64c7516 --- /dev/null +++ b/tests/test_reeval_live_weight.py @@ -0,0 +1,73 @@ +"""Test della rivalutazione del peso live (scripts/research/r0726_reeval_live_weight.py). + +Il punto del file non e' il numero: e' che il peso live resti **dentro il plateau** e che ogni +proposta di cambio passi da `weights_tilt_null`. Se un domani qualcuno alza SKH01 senza il gate, +questi test lo dicono. +""" +from __future__ import annotations + +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")) + +import r0726_reeval_live_weight as RW # noqa: E402 + + +def test_il_peso_live_e_nella_griglia_valutata(): + """Una griglia che non contiene il peso corrente non puo' dire se cambiarlo.""" + assert RW.W_LIVE in RW.W_GRID + + +def test_la_griglia_copre_entrambi_i_lati_del_peso_live(): + """Se la griglia fosse solo verso l'alto, l'argmax sarebbe garantito >= live per costruzione.""" + assert min(RW.W_GRID) < RW.W_LIVE < max(RW.W_GRID) + + +def test_il_peso_della_griglia_coincide_col_book_di_produzione(): + """W_LIVE deve essere il peso VERO del book Deribit, non una costante scollegata.""" + from src.live.book import W_SKH, W_TP01 + + assert RW.W_LIVE == pytest.approx(W_SKH) + assert W_TP01 + W_SKH == pytest.approx(1.0) + + +def test_la_statistica_e_la_mediana_delle_differenze_appaiate(): + """Lezione 26/07: fra offset si usa la mediana delle DIFFERENZE, non la differenza delle + mediane (che confronta offset diversi). Su dati costruiti le due risposte divergono.""" + # Serve che l'ORDINAMENTO differisca fra le due serie, altrimenti le due statistiche + # coincidono per caso (prima stesura di questo test: coincidevano, e non provava niente). + a = np.array([1.0, 5.0, 6.0]) + b = np.array([4.0, 2.0, 3.0]) + appaiata = float(np.median(a - b)) # [-3, +3, +3] -> +3 + delle_mediane = float(np.median(a) - np.median(b)) # 5 - 3 -> +2 + assert appaiata == pytest.approx(3.0) + assert delle_mediane == pytest.approx(2.0) + assert appaiata != pytest.approx(delle_mediane) + + +def test_gate_tilt_null_rifiuta_un_cambio_che_perde_in_sample(): + """Controllo POSITIVO sul gate: un tilt costruito per perdere pre-holdout deve fallire. + Un gate che approva tutto e' inutile quanto uno che rifiuta tutto.""" + from src.portfolio.portfolio import weights_tilt_null + + idx = pd.date_range("2019-01-01", periods=1500, freq="D", tz="UTC") + rng = np.random.default_rng(7) + buona = pd.Series(rng.normal(0.0008, 0.01, len(idx)), index=idx) + cattiva = pd.Series(rng.normal(-0.0008, 0.02, len(idx)), index=idx) + res = weights_tilt_null({"BUONA": buona, "CATTIVA": cattiva}, + {"BUONA": 0.75, "CATTIVA": 0.25}, + {"BUONA": 0.35, "CATTIVA": 0.65}, k_seen=9) + assert res["gate_pass"] is False + + +def test_promemoria_fee_coerente_con_la_regola_gia_decisa(): + """La soglia d'azione sulla fee vive in un solo posto: se cambia li', questo test la segue.""" + src = (ROOT / "scripts" / "research" / "r0726_reeval_live_weight.py").read_text() + assert "taker <=5bps" in src and ">10bps" in src