research(sol): SOL come terza gamba direzionale — SCARTATO, il guadagno e' un anno solo

Domanda dell'operatore dopo "hai gia' analizzato XRP/SOL/UNI?": si', ma sempre
dentro panieri cross-sectional su Hyperliquid. SOL e' l'UNICO dei tre eseguibile su
Deribit e TP01/SKH01 non erano mai stati misurati su di lui. Ipotesi a priori
registrata PRIMA di guardare: diluisce (trend multi-asset 19/06, corr 0.74).
Confermata.

DATO. Storico ricostruito da Deribit mainnet (SOL/USDC:USDC, 466.776 barre 5m dal
2022-03-15, 0 gap, resample maxΔ 0.00bps) e certificato: >1% da Coinbase nell'1.3%
(2022) e 0.6% (2023) delle barre, med 8.1 -> 3.9 bps dal 2022 al 2026; flat 1h 0.2%,
5m 20.8%. Conferma esatta del verdetto del 19/06. Da qui DUE LENTI dichiarate prima
di misurare: L-FULL (2022-03+) e L-PULITA (2024+).

GAMBE SOLE, meccanismi CONGELATI (nessuna ri-ottimizzazione su SOL): TP01 SOL Sh 0.91
con hold-out -0.24; SKH01 SOL Sh 0.51 con maxDD 40.5%. Quest'ultimo non e' un
dettaglio: SKH01-V2-DD fu SELEZIONATA il 23/06 sul criterio maxDD<30% (BTC 21%,
ETH 27%) -> su un asset nuovo fallisce il criterio per cui la variante esiste.

BOOK, 24 ancore, mediana delle differenze APPAIATE: dSharpe hold-out -0.166 e >0 in
0/24 ancore in ENTRAMBE le lenti. L-PULITA: dSharpe FULL -0.099 (0/24), dCAGR -1.34pp
(0/24). L-FULL: dSharpe FULL +0.094 (24/24) ma dCAGR +0.10pp.

IL DD SCENDE MA E' DE-LEVERING (5a occorrenza dopo VRP-DD, TP01xDVOL, MAT01, azioni
intere UCITS): a pari maxDD, su L-PULITA basta k=0.886 sul book a 2 gambe per avere
Sharpe 1.54 contro 1.30 e CAGR 14.5% contro 12.6%. L'unica lente in cui SOL aggiunge
e' quella costruita sui dati che la certificazione segnala.

E DENTRO QUELLA LENTE IL GUADAGNO E' UN ANNO: dSh 2022 -0.91 / 2023 +1.06 / 2024
-0.29 / 2025 -0.20 / 2026 -0.22 = 4 anni su 5 negativi. La gamba SOL da sola fa
Sh -1.99 / +2.65 / +0.45 / +0.17 / +1.46. Il 2023 e' la risalita post-FTX da ~$8 a
~$100: un evento, non un meccanismo. Corr col book +0.404 (L-FULL) / +0.561
(L-PULITA), vicina allo 0.74 che boccio' il trend multi-asset, e in salita man mano
che il dato migliora.

L'ESEGUIBILITA' NON E' IL VINCOLO: SOL_USDC-PERPETUAL ha min 0.001 SOL = $0.09 contro
il pavimento min_order $5. Primo candidato bocciato senza che il muro sia la taglia
del conto.

EFFETTO COLLATERALE TROVATO SU ME STESSO. Il "guardrail solo dati certi" dichiarato in
CLAUDE.md — load_data("SOL") -> FileNotFoundError — NON e' codice: load_data non ha
whitelist, solleva solo perche' il file non c'e'. Ricostruendo SOL in
data/raw/sol_1h.parquet il guardrail si e' disattivato in silenzio, e quel file non
viene rinfrescato dal cron (--asset BTC ETH) -> sarebbe diventato dato stantio con
l'aspetto di dato attivo. Riparato con la convenzione gia' in uso (hl_/eq_/eqx_/fut_):
SOL vive in data/raw/alt_sol_*.parquet. Congelato in due test, di cui uno DERIVA gli
asset a rischio da rebuild_history.DERIBIT_INSTR. La prima stesura di quel test
elencava i prefissi a mano e bocciava eqx_, fut_, vol_term_, fundnews_ (namespace
veri): l'invariante si deriva dal codice, non si elenca — stessa lezione di fee_watch.

Book, pesi, universo direzionale, config, cron: INVARIATI. Suite 631 verdi.
NB: test_gtaa_band_gate e' tornato VERDE da solo, senza modifiche al codice, perche' il
cron ha riscritto i parquet equity — la conferma in positivo della diagnosi del 07/08.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-08-22 07:11:16 +00:00
parent 8cfe15cbd5
commit eec76424ee
4 changed files with 575 additions and 0 deletions
+46
View File
@@ -1934,6 +1934,52 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
Misura che ha guidato una scelta: la banda del versamento suggerito resta **±1% da 1.200 a 3.000 Misura che ha guidato una scelta: la banda del versamento suggerito resta **±1% da 1.200 a 3.000
percorsi** → non domina il Monte Carlo ma la **granularita' della bisezione** (~€5) → percorsi percorsi** → non domina il Monte Carlo ma la **granularita' della bisezione** (~€5) → percorsi
tenuti bassi e incertezza **dichiarata**. tenuti bassi e incertezza **dichiarata**.
-**SOL COME TERZA GAMBA DIREZIONALE — SCARTATO (2026-08-22).** `scripts/research/r0822_sol_leg.py`,
test `tests/test_sol_leg.py` (5), diario `2026-08-22-sol-terza-gamba.md`. **Book/pesi/universo
direzionale/cron INVARIATI.** Nato da *"hai gia' analizzato XRP/SOL/UNI?"*: si', ma sempre dentro
panieri cross-sectional su HL — **SOL e' l'unico dei tre eseguibile su Deribit** e TP01/SKH01 non
erano mai stati misurati su di lui. Ipotesi a priori registrata **prima**: diluisce (trend
multi-asset 19/06, corr 0.74). Confermata.
(1) **Dato**: storico ricostruito (`SOL/USDC:USDC`, 466.776 barre 5m dal 2022-03-15, 0 gap) e
certificato → conferma esatta del 19/06: **>1% da Coinbase nell'1.3% (2022) / 0.6% (2023) delle
barre**, med 8.1→3.9 bps dal 2022 al 2026; flat 1h 0.2% (ok), 5m **20.8%** (run 177 barre).
Da qui **due lenti dichiarate prima di misurare**: L-FULL 2022-03+ e L-PULITA 2024+.
(2) **Gambe sole** (meccanismi CONGELATI): TP01 SOL Sh 0.91 / **hold-out 0.24**; SKH01 SOL
Sh 0.51 / **maxDD 40.5%**. ⚠️ **SKH01-V2-DD fu SELEZIONATA (23/06) sul criterio maxDD<30%**
(BTC 21%, ETH 27%): su un asset nuovo **il criterio per cui la variante esiste fallisce**
era una proprieta' dei due asset di taratura, non del meccanismo.
(3) **Book, 24 ancore, mediana delle differenze APPAIATE:** dSharpe hold-out **0.166, >0 in
0/24 ancore in ENTRAMBE le lenti**; L-PULITA dSharpe FULL 0.099 (0/24) e dCAGR 1.34pp (0/24);
L-FULL dSharpe FULL +0.094 (24/24) ma dCAGR +0.10pp. Canonica: 2 gambe 1.44/9.4%/14.7% →
3 gambe 1.48/7.5%/13.6% (L-FULL), 1.54 → **1.30** (L-PULITA).
(4) **Il DD scende ma e' DE-LEVERING (5ª occorrenza** dopo VRP-DD, TP01×DVOL, MAT01, azioni
intere UCITS**):** a pari maxDD, L-PULITA k=0.886 → Sh **1.54 vs 1.30** e CAGR 14.5% vs 12.6%.
**L'unica lente in cui SOL aggiunge e' quella costruita sui dati che la certificazione segnala.**
(5) 📌 **E dentro quella lente il guadagno e' UN ANNO:** dSh per anno 2022 **0.91** · 2023
**+1.06** · 2024 0.29 · 2025 0.20 · 2026 0.22 → **4 anni su 5 negativi**; la gamba SOL da sola
fa Sh 1.99 / **+2.65** / +0.45 / +0.17 / +1.46. Il 2023 e' la risalita post-FTX da ~$8 a ~$100:
un evento, non un meccanismo. Corr col book **+0.404 L-FULL / +0.561 L-PULITA** — vicina allo
0.74 che boccio' il trend multi-asset, **e in salita man mano che il dato migliora**.
(6) **L'eseguibilita' NON e' il vincolo:** `SOL_USDC-PERPETUAL` min 0.001 SOL = **$0.09** contro
il pavimento `min_order` $5 → **primo candidato bocciato senza che il muro sia la taglia del conto.**
(7) ⚠️ **EFFETTO COLLATERALE TROVATO SU ME STESSO: il "guardrail solo dati certi" NON e' codice.**
`load_data` **non ha whitelist**: `load_data("SOL") -> FileNotFoundError` funziona solo perche' il
file non c'e'. Ricostruendo SOL in `data/raw/sol_1h.parquet` **il guardrail si e' disattivato in
silenzio**, e quel file NON viene rinfrescato dal cron (`--asset BTC ETH`) → sarebbe diventato
**dato stantio con l'aspetto di dato attivo**. Riparato con la convenzione gia' in uso
(`hl_`/`eq_`/`eqx_`/`fut_`): SOL vive in **`data/raw/alt_sol_*.parquet`**, fuori dal namespace
nudo; guardrail verificato ripristinato e congelato in due test (uno **deriva** gli asset a
rischio da `rebuild_history.DERIBIT_INSTR`, l'altro segnala se `load_data` acquisisce una
whitelist vera). ⚠️ La prima stesura del test elencava i prefissi a mano e bocciava `eqx_`,
`fut_`, `vol_term_`, `fundnews_` — namespace veri: **l'invariante si deriva dal codice, non si
elenca** (stessa lezione di `fee_watch` il giorno prima).
**REGOLE:** (a) un meccanismo si trasferisce a un asset nuovo **coi parametri congelati**, o non
e' un trasferimento ma una nuova famiglia (→ `study_family_honest` + DSR); (b) **un criterio di
SELEZIONE va riprovato sull'asset nuovo** — quello di SKH01 non sopravvive; (c) quando il
risultato dipende dalla finestra, **guardare quale finestra la CERTIFICAZIONE approva**;
(d) **un contributo positivo si scompone per ANNO prima di crederci** — 24/24 ancore positive
sembravano un plateau ed erano un anno solo; (e) **ricostruire dati per un'analisi puo'
disattivare un guardrail che vive nell'assenza di un file**: chiedersi in quale namespace atterra.
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso: - **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. 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.** - **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
+141
View File
@@ -0,0 +1,141 @@
# 2026-08-22 — SOL come terza gamba: tutto il guadagno è un anno solo, e quell'anno il dato non è buono
**Richiesta:** dopo *"hai già provato ad analizzare utilizzo di XRP o SOL o UNI?"* — sì, ma sempre
dentro panieri cross-sectional su Hyperliquid. SOL è l'**unico dei tre eseguibile su Deribit** e
TP01/SKH01 non erano mai stati misurati su di lui. *"si fai una analisi"*.
**Esito: SCARTATO.** L'ipotesi a priori (*diluisce*) era registrata prima di guardare, ed è confermata.
**Toccati:** `scripts/research/r0822_sol_leg.py` (nuovo), `tests/test_sol_leg.py` (5), `CLAUDE.md`,
`data/raw/alt_sol_*.parquet` (namespace nuovo). **Book, pesi, `config/live.json`, cron: INVARIATI.**
---
## 0. Il dato, prima della strategia
Storico ricostruito da Deribit mainnet: `SOL/USDC:USDC`, **466.776 barre 5m dal 2022-03-15**,
0 gap, coerenza resample maxΔ 0.00 bps. Poi certificato — ed è la certificazione a decidere il
disegno dell'esperimento, non il contrario.
| | med bps vs Coinbase | p95 | max | >1% |
|---|---|---|---|---|
| 2022 | 8.1 | 40.1 | **2478.6** | 1.3% |
| 2023 | 7.6 | 30.0 | 1010.3 | 0.6% |
| 2024 | 5.9 | 22.7 | 109.4 | 0.0% |
| 2025 | 4.0 | 14.4 | 269.1 | 0.0% |
| 2026 | 3.9 | 12.4 | 51.7 | 0.0% |
Conferma esatta del verdetto del 19/06 (*«SOL 🟡 pulito da ~2024; 2022-23 rumoroso»*). Flat a 1h
**0,2%** (usabile), a 5m **20,8%** con run fino a 177 barre (~15h) — lontano da LTC (82%) ma non
è BTC/ETH (~0%).
Da qui **due lenti dichiarate prima di misurare**: `L-FULL` (2022-03+, usa gli anni segnalati) e
`L-PULITA` (2024+, dato buono ma quasi tutto hold-out).
## 1. Le gambe da sole (meccanismi CONGELATI, nessuna ri-ottimizzazione)
| gamba | Sharpe | maxDD | CAGR | Sh hold-out |
|---|---|---|---|---|
| TP01 BTC | 1.13 | 16.2% | 15.2% | +0.28 |
| TP01 ETH | 1.10 | 15.2% | 15.2% | +0.62 |
| **TP01 SOL** | **0.91** | 12.7% | 9.8% | **0.24** |
| SKH01 BTC | 1.28 | 21.4% | 33.6% | +1.49 |
| SKH01 ETH | 1.05 | 27.4% | 30.9% | +1.49 |
| **SKH01 SOL** | **0.51** | **40.5%** | 11.3% | +1.05 |
⚠️ **SKH01-V2-DD su SOL fa 40,5% di drawdown.** La variante «-DD» esiste *perché* fu selezionata,
nella 2ª ondata del 23/06, sul criterio **maxDD < 30%** (BTC 21%, ETH 27%). Su un asset nuovo il
criterio stesso per cui la variante è stata scelta **fallisce**. Non è un dettaglio: è la firma di
un parametro tarato su due asset e presentato come proprietà del meccanismo.
## 2. Il livello che conta: il book
24 ancore, **mediana delle differenze appaiate** (regola 26/07: un Δ eredita la fortuna d'ancora).
| | L-FULL (2022-03+) | L-PULITA (2024+) |
|---|---|---|
| dSharpe FULL | **+0.094** (>0 in **24/24**) | **0.099** (>0 in **0/24**) |
| dSharpe hold-out | **0.166** (>0 in **0/24**) | **0.166** (>0 in **0/24**) |
| DD più basso di | +1.435 pp (24/24) | +1.470 pp (24/24) |
| d CAGR | +0.105 pp (58%) | **1.339 pp** (0/24) |
All'ancora canonica: 2 gambe Sh 1.44 / DD 9.4% / CAGR 14.7% → 3 gambe Sh 1.48 / DD 7.5% /
CAGR 13.6% su L-FULL; su L-PULITA 1.54 → **1.30**.
## 3. Il drawdown scende — ma è de-levering (5ª occorrenza)
Aggiungere SOL abbassa DD **e** CAGR insieme. Il null codificato chiede: *lo stesso DD si compra
più a buon mercato riducendo la size?* Si cerca `k` con `maxDD(k·book_2gambe) = maxDD(book_3gambe)`:
| lente | k | Sh 3 gambe | Sh 2 gambe × k | CAGR 3g | CAGR 2g × k | verdetto |
|---|---|---|---|---|---|---|
| L-FULL | 0.793 | 1.48 | 1.44 | 13.6% | 11.5% | SOL aggiunge |
| **L-PULITA** | 0.886 | **1.30** | **1.54** | 12.6% | 14.5% | **de-levering** |
**L'unica lente in cui SOL aggiunge è quella costruita sui dati che la certificazione segnala.**
## 4. E dentro quella lente, il guadagno è un anno
| anno | 2g Sh | 3g Sh | dSh | dCAGR |
|---|---|---|---|---|
| 2022 | 1.37 | 0.46 | **0.91** | 6.0 pp |
| **2023** | 1.34 | 2.40 | **+1.06** | **+11.1 pp** |
| 2024 | 1.65 | 1.36 | 0.29 | 5.6 pp |
| 2025 | 1.00 | 0.80 | 0.20 | 2.7 pp |
| 2026 | 2.70 | 2.48 | 0.22 | 2.9 pp |
Gamba SOL da sola: 2022 **1.99** · 2023 **+2.65** · 2024 +0.45 · 2025 +0.17 · 2026 +1.46.
**4 anni su 5 negativi.** Il 2023 è l'anno in cui SOL risalì da ~$8 a ~$100 dopo il crollo FTX: un
trend-follower su quel movimento guadagna una fortuna **una volta sola**. Non è un meccanismo che
trasferisce, è un evento.
E la correlazione col book attuale è **+0.404** (L-FULL) e **+0.561** (L-PULITA) — vicina allo
0.74 per cui il trend multi-asset fu scartato il 19/06, **e in salita** man mano che il dato
migliora.
## 5. Ciò che non è il vincolo
**L'eseguibilità non c'entra:** `SOL_USDC-PERPETUAL` ha min 0.001 SOL = **$0.09**, contro il
pavimento `min_order` di $5 del book. SOL sarebbe tradabile oggi a $644. È la prima volta in questo
progetto che un candidato viene bocciato **senza** che il muro sia la taglia del conto.
## 6. ⚠️ Effetto collaterale del mio stesso lavoro: un guardrail disattivato in silenzio
CLAUDE.md dichiara: *«`load_data("SOL", ...)` → FileNotFoundError (guardrail: solo dati certi)»*.
**Non è codice.** `load_data` non ha alcuna whitelist: solleva l'eccezione solo perché il file non
esiste. Ricostruendo lo storico in `data/raw/sol_1h.parquet` **ho disattivato il guardrail senza
che nulla lo segnalasse** — e quel file non sarebbe stato rinfrescato dal cron (`--asset BTC ETH`),
quindi sarebbe diventato dato stantio con l'aspetto di dato attivo.
È la stessa forma dei tre difetti dei giorni scorsi: un controllo che funziona per configurazione,
non per costruzione.
**Riparato con la convenzione che il progetto già usa** (`hl_`, `eq_`, `eqx_`, `fut_`): SOL vive in
`data/raw/alt_sol_*.parquet`, fuori dal namespace nudo. Guardrail verificato ripristinato.
⚠️ **Errore mio nel test che doveva sorvegliarlo:** la prima stesura elencava a mano i prefissi
leciti e bocciava `eqx_`, `fut_`, `vol_term_`, `fundnews_` — namespace veri che non conoscevo.
Riscritto per **derivare** gli asset a rischio da `rebuild_history.DERIBIT_INSTR`: l'invariante non
è «quali prefissi esistono» (lista che invecchia) ma «quali asset possono finire nel namespace
nudo», che sta nel codice. Stessa lezione di `fee_watch` il giorno prima.
## VERDETTO
**SOL come terza gamba direzionale: SCARTATO.** Hold-out negativo a 24/24 ancore in entrambe le
lenti; sulla finestra pulita peggiora tutto; il DD migliore è de-levering; il guadagno della
finestra sporca è un anno solo; la correlazione col book sale col migliorare del dato. **Book,
pesi, universo direzionale invariati.**
## REGOLE
- **Un meccanismo si trasferisce a un asset nuovo con i parametri CONGELATI, o non è un
trasferimento ma una nuova famiglia** — e allora servono `study_family_honest` e deflated-Sharpe.
- **Un criterio di selezione va riprovato sull'asset nuovo:** SKH01-V2-DD fu scelta per maxDD<30%
e su SOL fa 40,5%. Il criterio era una proprietà dei due asset su cui fu tarato, non del sistema.
- **Quando un risultato dipende dalla finestra, guardare quale finestra la CERTIFICAZIONE
approva** — qui le due si contraddicono e la lente che promuove è quella non certificabile.
- **Un contributo positivo si scompone per anno prima di crederci:** +0.094 di Sharpe mediano
sembra un plateau (24/24 ancore) ed è un anno solo con quattro anni negativi intorno.
- **Ricostruire dati per un'analisi può disattivare un guardrail** che vive nell'assenza di un
file. Prima di scaricare un asset fuori universo, chiedersi in quale namespace atterra.
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python
"""r0822_sol_leg.py — SOL come TERZA GAMBA direzionale del book Deribit: aggiunge o diluisce?
DOMANDA (operatore, 2026-08-22): XRP/SOL/UNI sono gia' stati analizzati, ma sempre dentro panieri
cross-sectional su Hyperliquid. SOL e' l'UNICO dei tre eseguibile su Deribit (SOL_USDC-PERPETUAL,
min 0.001 SOL ~ $0.08, molto sotto il pavimento $5 del book) e TP01/SKH01 non sono mai stati
misurati su di lui. Questo script chiude il buco.
DISEGNO — cio' che NON si fa qui:
* NESSUNA ri-ottimizzazione su SOL. I meccanismi sono CONGELATI (TP01 `CANONICAL`,
SKH01 `SKH01_V2_DD`). Ritarare i parametri su un asset nuovo sarebbe una nuova famiglia e
richiederebbe `study_family_honest` + deflated-Sharpe; qui la domanda e' se il meccanismo
ESISTENTE trasferisce, che e' una domanda diversa e piu' onesta.
* NESSUNA scelta di finestra a posteriori: le due lenti sono dichiarate PRIMA (sotto).
IPOTESI A PRIORI (registrata prima di guardare): DILUISCE. Il test trend-multi-asset del
2026-06-19 trovo' corr 0.74 con TP01 -> ridondante; SOL e' un beta alto di BTC. Se il risultato
fosse "aggiunge", va guardato con piu' sospetto, non con meno.
DUE LENTI, dichiarate prima (la certificazione le impone, non le sceglie il risultato):
L-FULL finestra comune 2022-03+ -> usa 2022-23, che la certificazione segnala come rumorosi
(>1% da Coinbase nell'1.3% / 0.6% delle barre)
L-PULITA finestra 2024+ -> SOL a med 4-6 bps da Coinbase, ma e' quasi tutta hold-out
ANCORA: ogni numero daily e' calcolato su TUTTE E 24 le ancore e riportato come MEDIANA DELLE
DIFFERENZE APPAIATE (regola codificata 2026-07-26: un Delta fra due varianti eredita la fortuna
d'ancora come ogni altro numero, e la mediana delle differenze != differenza delle mediane).
uv run python scripts/research/r0822_sol_leg.py [--anchors N]
"""
from __future__ import annotations
import sys
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 r0702_tp01_offset import resample_offset # noqa: E402 (importato, non riscritto)
from src.backtest.harness import backtest_signals # noqa: E402
from src.data.downloader import load_data # noqa: E402 (solo BTC/ETH: vedi _alt sotto)
from src.strategies.skyhook import SKH01_V2_DD, build_frames, skyhook_entries # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
# ⚠️ SOL vive in un NAMESPACE DEDICATO `alt_sol_*.parquet`, non in `sol_*.parquet`.
# Motivo (trovato in questa sessione): il "guardrail solo dati certi" documentato in CLAUDE.md
# — `load_data("SOL", ...) -> FileNotFoundError` — non e' codice, e' l'ASSENZA del file:
# `load_data` non ha alcuna whitelist. Ricostruendo lo storico di SOL in `data/raw/sol_*.parquet`
# il guardrail si disattiva IN SILENZIO, e per giunta quel file NON viene rinfrescato dal cron
# (`cron_daily.sh` fa `--asset BTC ETH`) -> diventerebbe dato stantio che sembra attivo.
# Il namespace dedicato e' la convenzione che il progetto usa gia' per `hl_` e `eq_`.
# Per rigenerarlo: uv run python scripts/analysis/rebuild_history.py --asset SOL
# poi rinominare sol_*.parquet -> alt_sol_*.parquet
ALT_NS = "alt_"
def load_alt(asset: str, tf: str) -> pd.DataFrame:
"""Legge un asset del namespace di ricerca (fuori dal feed attivo BTC/ETH)."""
path = ROOT / "data" / "raw" / f"{ALT_NS}{asset.lower()}_{tf}.parquet"
if not path.exists():
raise FileNotFoundError(
f"{path} assente. Rigenera con:\n"
f" uv run python scripts/analysis/rebuild_history.py --asset {asset}\n"
f" poi rinomina data/raw/{asset.lower()}_*.parquet -> {ALT_NS}{asset.lower()}_*.parquet")
df = pd.read_parquet(path)
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
return df
def dati(asset: str, tf: str) -> pd.DataFrame:
"""BTC/ETH dal feed certificato attivo; tutto il resto dal namespace di ricerca."""
return load_data(asset, tf) if asset in ("BTC", "ETH") else load_alt(asset, tf)
DUE = ("BTC", "ETH")
TRE = ("BTC", "ETH", "SOL")
W_TP, W_SKH = 0.75, 0.25 # pesi del book live Deribit
PULITA = pd.Timestamp("2024-01-01", tz="UTC") # dalla certificazione, non dal risultato
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
ANNO = 365.0
def sh(s: pd.Series) -> float:
s = s.dropna()
return float(s.mean() / s.std() * np.sqrt(ANNO)) if len(s) > 30 and s.std() > 0 else float("nan")
def maxdd(s: pd.Series) -> float:
e = (1 + s.fillna(0)).cumprod()
return float((e / e.cummax() - 1).min())
def cagr(s: pd.Series) -> float:
s = s.dropna()
if len(s) < 30:
return float("nan")
return float((1 + s).prod() ** (ANNO / len(s)) - 1)
@lru_cache(maxsize=8)
def _h1(asset: str) -> pd.DataFrame:
return dati(asset, "1h")
@lru_cache(maxsize=128)
def tp01_leg(asset: str, h: int) -> pd.Series:
"""TP01 CONGELATO su un asset, ancora h. Rendimenti netti giornalieri, causale."""
df = resample_offset(_h1(asset), h)
tp = TrendPortfolio(**CANONICAL)
px = df["close"].values.astype(float)
r = np.zeros(len(px)); r[1:] = px[1:] / px[:-1] - 1.0
tgt = tp.target_series(df)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
return pd.Series(np.clip(net, -0.99, None),
index=pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)))
@lru_cache(maxsize=8)
def skh_leg(asset: str) -> pd.Series:
"""SKH01-V2-DD CONGELATO su un asset, riportato su griglia giornaliera (come lo sleeve)."""
ltf, htf = build_frames(dati(asset, "5m"))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
m = backtest_signals(ltf, ent, fee_rt=0.001, leverage=1.0, asset=asset, tf="230m")
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
return s.resample("1D").last().ffill().pct_change().dropna()
def ew(series: dict) -> pd.Series:
"""Equal-weight sulle date COMUNI (inner): un book a N gambe si valuta dove ci sono tutte."""
J = pd.concat(series, axis=1, join="inner").dropna()
return J.mean(axis=1)
def book(assets: tuple, h: int) -> pd.Series:
tp = ew({a: tp01_leg(a, h) for a in assets})
sk = ew({a: skh_leg(a) for a in assets})
J = pd.concat({"tp": tp, "sk": sk}, axis=1, join="inner").fillna(0.0)
return W_TP * J["tp"] + W_SKH * J["sk"]
def k_iso_dd(base: pd.Series, target: pd.Series, lo: float = 0.01, hi: float = 1.0) -> float:
"""Fattore k per cui `k * base` ha lo STESSO maxDD di `target`. PURA.
E' il cuore del NULL DEL DE-LEVERING, che in questo progetto e' il primo test dovuto a ogni
claim "meno drawdown" (5a occorrenza: VRP-DD 03/07, TP01xDVOL 26/06, MAT01 25/07, azioni
intere UCITS 26/07, e SOL qui). maxDD e' monotono in k, quindi la bisezione converge.
"""
obiettivo = maxdd(target)
for _ in range(60):
k = 0.5 * (lo + hi)
if maxdd(k * base) < obiettivo: # piu' profondo del bersaglio -> ridurre la size
hi = k
else:
lo = k
return 0.5 * (lo + hi)
def taglia(s: pd.Series, start=None) -> pd.Series:
return s if start is None else s[s.index >= start]
def riga(nome: str, s: pd.Series) -> str:
return (f" {nome:<26}{len(s):>7}{sh(s):>9.2f}{maxdd(s)*100:>9.1f}%{cagr(s)*100:>9.1f}%"
f"{sh(taglia(s, HOLDOUT)):>10.2f}")
def main() -> None:
n_anc = 24
if "--anchors" in sys.argv:
n_anc = int(sys.argv[sys.argv.index("--anchors") + 1])
anchors = list(range(0, 24, max(1, 24 // n_anc)))[:n_anc]
print("=" * 104)
print(" r0822 — SOL COME TERZA GAMBA DEL BOOK DERIBIT (meccanismi CONGELATI)")
print("=" * 104)
print(" ipotesi a priori dichiarata: DILUISCE (trend multi-asset 19/06: corr 0.74 con TP01)")
print(f" ancore valutate: {len(anchors)} pesi book TP01 {W_TP} / SKH01 {W_SKH}")
# ---------------------------------------------------------------- 1. le gambe da sole
print("\n [1/5] LE GAMBE DA SOLE (ancora canonica h=0, finestra propria di ogni asset)")
print(f"\n {'gamba':<26}{'barre':>7}{'Sharpe':>9}{'maxDD':>10}{'CAGR':>9}{'Sh hold':>10}")
for a in TRE:
print(riga(f"TP01 {a}", tp01_leg(a, 0)))
for a in TRE:
print(riga(f"SKH01 {a}", skh_leg(a)))
# ---------------------------------------------------------------- 2. SOL nelle due lenti
print("\n [2/5] LA GAMBA SOL NELLE DUE LENTI DICHIARATE")
solb = W_TP * tp01_leg("SOL", 0).reindex(skh_leg("SOL").index).fillna(0.0) \
+ W_SKH * skh_leg("SOL")
print(f"\n {'lente':<26}{'barre':>7}{'Sharpe':>9}{'maxDD':>10}{'CAGR':>9}{'Sh hold':>10}")
print(riga("SOL book-leg L-FULL", solb))
print(riga("SOL book-leg L-PULITA", taglia(solb, PULITA)))
b2 = book(DUE, 0)
comune = solb.index.intersection(b2.index)
if len(comune) > 100:
c = float(np.corrcoef(solb.loc[comune], b2.loc[comune])[0, 1])
cp = comune[comune >= PULITA]
c2 = float(np.corrcoef(solb.loc[cp], b2.loc[cp])[0, 1]) if len(cp) > 100 else float("nan")
print(f"\n corr(gamba SOL, book attuale BTC/ETH): L-FULL {c:+.3f} L-PULITA {c2:+.3f}")
print(" (riferimento: il trend multi-asset del 19/06 fu scartato a corr 0.74)")
# ---------------------------------------------------------------- 3. book 2 vs 3 gambe
print("\n [3/5] BOOK A 2 GAMBE vs 3 GAMBE — differenze APPAIATE per ancora")
for nome, start in (("L-FULL (2022-03+)", None), ("L-PULITA (2024+)", PULITA)):
d_sh, d_dd, d_cagr, d_hold = [], [], [], []
for h in anchors:
A, B = book(DUE, h), book(TRE, h)
idx = A.index.intersection(B.index) # stessa finestra: SOL parte dopo
A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start)
if len(A) < 200:
continue
d_sh.append(sh(B) - sh(A)); d_dd.append((maxdd(B) - maxdd(A)) * 100)
d_cagr.append((cagr(B) - cagr(A)) * 100)
d_hold.append(sh(taglia(B, HOLDOUT)) - sh(taglia(A, HOLDOUT)))
if not d_sh:
print(f"\n {nome}: campione insufficiente")
continue
print(f"\n {nome} ({len(d_sh)} ancore)")
for lab, v, u in (("dSharpe FULL", d_sh, ""), ("dSharpe hold-out", d_hold, ""),
("DD piu' BASSO di", d_dd, " pp"), ("d CAGR", d_cagr, " pp")):
v = np.array(v, float)
pos = np.mean(v > 0) * 100
print(f" {lab:<20} mediana {np.median(v):+7.3f}{u} "
f"banda [{np.percentile(v, 10):+.3f}, {np.percentile(v, 90):+.3f}] "
f">0 nel {pos:.0f}% delle ancore")
# ---------------------------------------------------------------- 3-bis. per anno
print("\n [3bis] DOVE NASCE IL GUADAGNO — book 2 vs 3 gambe, anno per anno (ancora h=0)")
A0, B0 = book(DUE, 0), book(TRE, 0)
i0 = A0.index.intersection(B0.index); A0, B0 = A0.loc[i0], B0.loc[i0]
print(f"\n {'anno':<7}{'2g Sh':>8}{'3g Sh':>8}{'dSh':>8}{'2g CAGR':>10}{'3g CAGR':>10}{'dCAGR':>9}")
for y in sorted(set(A0.index.year)):
a, b = A0[A0.index.year == y], B0[B0.index.year == y]
if len(a) < 60:
continue
print(f" {y:<7}{sh(a):>8.2f}{sh(b):>8.2f}{sh(b)-sh(a):>+8.2f}"
f"{cagr(a)*100:>9.1f}%{cagr(b)*100:>9.1f}%{(cagr(b)-cagr(a))*100:>+8.1f}pp")
solleg = W_TP * tp01_leg("SOL", 0).reindex(skh_leg("SOL").index).fillna(0.0) + W_SKH * skh_leg("SOL")
print(f"\n la sola gamba SOL: " + " ".join(
f"{y} Sh {sh(solleg[solleg.index.year == y]):+.2f}"
for y in sorted(set(solleg.index.year)) if len(solleg[solleg.index.year == y]) >= 60))
# ---------------------------------------------------------------- 4. null de-levering
print("\n [4/5] NULL DEL DE-LEVERING — il primo test dovuto a ogni claim 'meno drawdown'")
print(" Aggiungere SOL abbassa DD *e* CAGR insieme. La domanda non e' 'ha meno DD?' ma")
print(" 'lo stesso DD si compra piu' a buon mercato semplicemente riducendo la size?'.")
print(" Si cerca k tale che maxDD(k * book_2gambe) == maxDD(book_3gambe), poi si confronta.")
print(f"\n {'lente':<20}{'k':>7}{'Sh 3gambe':>11}{'Sh 2g x k':>11}{'CAGR 3g':>10}"
f"{'CAGR 2g x k':>13}{'verdetto':>14}")
for nome, start in (("L-FULL", None), ("L-PULITA", PULITA)):
A, B = book(DUE, 0), book(TRE, 0)
idx = A.index.intersection(B.index)
A, B = taglia(A.loc[idx], start), taglia(B.loc[idx], start)
k = k_iso_dd(A, B)
sh3, sh2 = sh(B), sh(k * A)
v = "SOL AGGIUNGE" if sh3 > sh2 + 0.02 else ("de-levering" if sh2 > sh3 + 0.02 else "pari")
print(f" {nome:<20}{k:>7.3f}{sh3:>11.2f}{sh2:>11.2f}{cagr(B)*100:>9.1f}%"
f"{cagr(k*A)*100:>12.1f}%{v:>14}")
# ---------------------------------------------------------------- 5. eseguibilita'
print("\n [5/5] ESEGUIBILITA' a $644 (equity reale del conto)")
px = float(dati("SOL", "1h")["close"].iloc[-1])
print(f" SOL_USDC-PERPETUAL min 0.001 SOL = ${0.001*px:.3f} << pavimento min_order $5")
print(f" a 3 gambe il target per asset scende da ~1/2 a ~1/3 dell'esposizione:")
for eq in (644.0, 2000.0, 6050.0):
print(f" equity ${eq:>7,.0f}: 2 gambe ${eq*0.5*0.5:>7,.0f}/asset "
f"3 gambe ${eq*0.5/3:>7,.0f}/asset")
print("\n" + "=" * 104)
if __name__ == "__main__":
main()
+110
View File
@@ -0,0 +1,110 @@
"""Test dell'analisi "SOL come terza gamba" (scripts/research/r0822_sol_leg.py, 2026-08-22).
Congela il MECCANISMO e le DECISIONI, non i numeri: i numeri stanno nel diario e li riproduce
lo script (che richiede il namespace di ricerca `data/raw/alt_sol_*.parquet`, rigenerabile).
Un test che dipendesse da quei parquet fallirebbe su una macchina pulita — e la lezione del
2026-08-07 (`test_gtaa_band_gate` rosso perche' il cron riscriveva i dati sotto) dice di non
legare un test a dati non versionati.
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
# ===========================================================================
# NULL DEL DE-LEVERING — il pezzo riusabile (5a occorrenza nel progetto)
# ===========================================================================
def test_k_iso_dd_trova_il_fattore_che_pareggia_il_drawdown():
from r0822_sol_leg import k_iso_dd, maxdd
rng = np.random.default_rng(822)
idx = pd.date_range("2022-01-01", periods=1500, freq="D", tz="UTC")
base = pd.Series(rng.normal(0.0006, 0.02, len(idx)), index=idx)
target = 0.6 * base # per costruzione k deve tornare ~0.6
k = k_iso_dd(base, target)
assert abs(k - 0.6) < 0.02, f"k atteso ~0.60, ottenuto {k:.3f}"
assert abs(maxdd(k * base) - maxdd(target)) < 1e-4
def test_il_null_smaschera_un_falso_diversificatore():
"""Controllo POSITIVO: una serie che e' solo la stessa cosa a size ridotta DEVE risultare
'de-levering', altrimenti il null non ha potenza e assolverebbe qualunque claim."""
from r0822_sol_leg import k_iso_dd, sh
rng = np.random.default_rng(1)
idx = pd.date_range("2022-01-01", periods=1500, freq="D", tz="UTC")
A = pd.Series(rng.normal(0.0008, 0.02, len(idx)), index=idx)
finto = 0.7 * A # nessuna informazione nuova
k = k_iso_dd(A, finto)
assert abs(sh(finto) - sh(k * A)) < 0.05, "a pari DD un de-levering puro ha lo STESSO Sharpe"
# e un vero diversificatore (rumore indipendente) deve invece BATTERE il de-levering
B = pd.Series(rng.normal(0.0008, 0.02, len(idx)), index=idx)
vero = 0.5 * A + 0.5 * B
k2 = k_iso_dd(A, vero)
assert sh(vero) > sh(k2 * A) + 0.05, "il null deve lasciar passare un diversificatore vero"
# ===========================================================================
# DECISIONI congelate
# ===========================================================================
def test_l_universo_direzionale_resta_BTC_ETH():
"""SOL come terza gamba e' stato misurato il 2026-08-22 ed e' SCARTATO (diluisce).
Se qualcuno aggiunge un asset a `sleeves.ASSETS`, questo test glielo ricorda: la domanda
e' gia' stata posta e la risposta era no. Rifarla richiede r0822_sol_leg.py, non un'opinione.
"""
from src.portfolio.sleeves import ASSETS
assert tuple(ASSETS) == ("BTC", "ETH"), (
"universo direzionale cambiato: rifare l'analisi marginale (r0822_sol_leg.py) "
"e il null del de-levering prima di toccare il book live")
def test_nessun_asset_fuori_universo_nel_namespace_NUDO():
"""⚠️ Il 'guardrail solo dati certi' di CLAUDE.md — load_data('SOL') -> FileNotFoundError —
NON e' codice: `load_data` non ha whitelist, funziona solo perche' il file non c'e'.
Ricostruendo SOL in `data/raw/sol_*.parquet` il guardrail si disattiva IN SILENZIO, e quel
file non viene rinfrescato dal cron (che fa `--asset BTC ETH`) -> dato stantio che sembra
attivo. La ricerca su asset fuori universo usa un namespace prefissato (`alt_`, come
`hl_`/`eq_`/`eqx_`/`fut_`).
⚠️ La prima stesura di questo test elencava a mano i prefissi leciti e bocciava `eqx_`,
`fut_`, `vol_term_`, `fundnews_` — namespace veri che non conoscevo. L'invariante non e'
'quali prefissi esistono' (lista che invecchia) ma 'quali asset `rebuild_history` puo'
scrivere nel namespace nudo', che si DERIVA dal codice.
"""
from scripts.analysis.rebuild_history import DERIBIT_INSTR
from src.data.downloader import ASSETS as ATTIVI
raw = ROOT / "data" / "raw"
if not raw.exists():
return
scrivibili = set(DERIBIT_INSTR) | set(ATTIVI)
intrusi = sorted(
f"{a.lower()}_{tf}.parquet"
for a in scrivibili if a not in ("BTC", "ETH")
for tf in ("5m", "15m", "1h", "1d")
if (raw / f"{a.lower()}_{tf}.parquet").exists())
assert not intrusi, (
f"asset fuori universo nel namespace NUDO di data/raw: {intrusi}. Li' `load_data` li "
"raggiunge (nessuna whitelist) e il cron non li rinfresca: rinominarli con un prefisso "
"di ricerca, es. alt_<asset>_<tf>.parquet (vedi r0822_sol_leg.py).")
def test_il_guardrail_di_load_data_e_per_ASSENZA_non_per_codice():
"""Congela il FATTO, cosi' non va riscoperto: se un giorno `load_data` prendesse una
whitelist vera, questo test lo segnala e la nota qui sopra va aggiornata."""
import inspect
from src.data.downloader import load_data
src = inspect.getsource(load_data)
assert "FileNotFoundError" in src
assert "ASSETS" not in src, (
"load_data sembra avere ora una whitelist: bene, ma aggiorna la nota di r0822_sol_leg.py "
"e di CLAUDE.md, che dichiarano il guardrail come 'per assenza del file'")