Files
Adriano Dal Pastro eec76424ee 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>
2026-08-22 07:11:16 +00:00

279 lines
14 KiB
Python

#!/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()