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:
@@ -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'")
|
||||
Reference in New Issue
Block a user