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
+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'")