"""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__.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'")