"""r0823_data_unused — §43 DATA-UNUSED: i dati che il progetto possiede e nessuno legge (2026-08-23). DOMANDA (ondata `research/wave-0822`, filone 43): *"nuove strategie possibili"*. La fonte piu' probabile di una strategia nuova non e' un'idea nuova sui soliti dati: e' un dato che nessuno ha ancora guardato. Due fasi, la seconda dipende dalla prima. FASE 1 — INVENTARIO (e' gia' un risultato, non un preambolo). Censisce TUTTO cio' che sta su disco sotto `data/` e, per ogni dataset a schema ricco, quale COLONNA e' letta da `src/` o da uno script di ricerca e quale non lo e' MAI. La lettura non e' greppata a mano: si indicizzano una volta tutti i `.py` di src/ scripts/ tests/ (escluso `Old/`, che e' archivio) e si cerca il nome della colonna come sottostringa. E' una misura GENEROSA verso "letta" (un nome comune come `close` combacia ovunque) -> quando dice MAI LETTA, e' un fatto. FASE 2 — UNA misura, la migliore. Dall'inventario esce un solo candidato che soddisfa tutti e quattro i criteri richiesti: (i) piu' storia utilizzabile, (ii) meccanismo economico dicibile in una frase, (iii) eseguibile su Deribit a $635, (iv) non gia' chiuso dal progetto. E' `FlowInExNtv`/`FlowOutExNtv` di CoinMetrics (`data/external/coinmetrics/cm_{btc,eth}.csv`): **0 file del repo li leggono**, copertura 100% dal 2018, entrambi gli asset. ⚠️ IL PUNTO DELICATO, DICHIARATO IN TESTA. La famiglia "exchange-flow" e' stata UCCISA il 2026-07-24 (`r0724_onchain_wave.py`, famiglia EXS, 0/6 slot). Riaprirla richiede un meccanismo NUOVO, non un secondo tentativo. Il meccanismo nuovo qui e' ARITMETICO e si MISURA prima di usarlo (blocco 1): - EXS ha testato `SplyExNtv`, lo STOCK di monete sugli exchange; - `FlowIn - FlowOut` e' la DERIVATA di quello stock -> stessa informazione (lo si misura: corr con d(SplyEx) attesa ~1); - `FlowIn + FlowOut`, il turnover LORDO, si CANCELLA nella differenza -> e' algebricamente ASSENTE da cio' che e' stato testato (corr con d(SplyEx) attesa ~0). Quindi la parte onestamente nuova del dato e' il LORDO, non il netto. Meccanismo in una frase: *il turnover lordo sugli exchange misura quanta parte del flottante viene ri-posizionata, cioe' la partecipazione REGOLATA, a prescindere dalla direzione in cui si agisce.* E siccome "riaprire un parametro riapre la sua famiglia" (regola 2026-07-30), il conteggio dei trial e' fatto AL RIALZO: 36 celle nuove, piu' le 4 gia' spese su EXS il 24/07 -> il deflated Sharpe si riporta a N=36 E a N=40, e si pubblica la sensibilita' del verdetto al conteggio. Controllo positivo obbligatorio: la famiglia UCCISA viene ri-girata con la SUA factory (`r0724_onchain_wave.exs_factory`, importata, non riscritta). Se il macchinario non riproduce "earns_slot=False" sul caso noto, non e' credibile su quello nuovo. CAUSALITA'. Riusa `r0724_onchain_wave._to_target` e `AVAIL_LAG_D=1` -> lag totale attivita' -> posizione = 2 giorni (1 di disponibilita' del dato + 1 di `eval_weights`). Non e' una scelta di oggi: e' la convenzione gia' congelata per questa fonte. FINESTRA IN BARRE ATTIVE, non di calendario. Il CSV CoinMetrics community si ferma al 2026-05-24: dopo quella data il segnale e' assente e la posizione e' FLAT. Contare quei giorni come evidenza sarebbe l'errore gia' pagato dal progetto (94% di zeri letti come "nessuna perdita"). Qui la finestra si riporta in barre con posizione != 0. COSA NON FA: non manda ordini, non scrive nulla, non tocca `src/` `config/` `scripts/live/` `data/paper_*`. Sola lettura. Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823_data_unused.py (aggiungere --skip-inventory per la sola FASE 2) """ from __future__ import annotations import json import re import sys from pathlib import Path import numpy as np import pandas as pd from scipy import stats ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) sys.path.insert(0, str(ROOT / "scripts" / "research")) import altlib as al # noqa: E402 from altlib import (causality_ok, deflated_sharpe, eval_weights_smallcap, # noqa: E402 fmt_marginal, implausible_sharpe, study_family_honest) import r0724_onchain_wave as ow # noqa: E402 (loader + causalita' + factory della famiglia uccisa) DATA = ROOT / "data" RAW = DATA / "raw" LINE = "=" * 100 # =========================================================================== # FASE 1 — INVENTARIO # =========================================================================== def _index_repo() -> dict[str, str]: """Tutti i .py di src/ scripts/ tests/ in memoria una volta sola. `Old/` e' ARCHIVIO (il progetto lo dichiara non fidato) -> escluso: un dato letto solo li' NON e' letto.""" idx = {} for d in ("src", "scripts", "tests"): for p in (ROOT / d).rglob("*.py"): if "Old/" in str(p): continue try: idx[str(p.relative_to(ROOT))] = p.read_text(errors="ignore") except OSError: pass return idx def _readers(idx: dict[str, str], needle: str, exclude_self: bool = True, word: bool = True) -> list[str]: """`word=True` -> confini di parola. La prima stesura cercava la SOTTOSTRINGA e dava `iv` in 571 file (e' dentro "drive", "receive", ...): un conteggio che non misura niente. Resta comunque una misura GENEROSA verso "letta" (una variabile locale omonima conta) -> e' quando dice MAI LETTA che l'affermazione e' forte.""" pat = re.compile(rf"(? tuple[int, list[str], str]: try: d = pd.read_parquet(p) except Exception as e: # noqa: BLE001 return 0, [f""], "" rng = "" for c in d.columns: if pd.api.types.is_datetime64_any_dtype(d[c]): rng = f"{pd.Timestamp(d[c].min()).date()}->{pd.Timestamp(d[c].max()).date()}" break else: if isinstance(d.index, pd.DatetimeIndex) and len(d): rng = f"{d.index.min().date()}->{d.index.max().date()}" elif "timestamp" in d.columns and len(d): t = pd.to_datetime(d["timestamp"], unit="ms", utc=True) rng = f"{t.min().date()}->{t.max().date()}" return len(d), list(d.columns), rng # famiglie di file: (etichetta, glob, campione su cui leggere lo schema, CHIAVE di ricerca). # La chiave e' ESPLICITA e non derivata dall'etichetta: derivandola, due famiglie realmente # lette (`eqx_`, `fund_`) risultavano "NESSUNO" — un falso negativo che avrebbe fabbricato # un dato "mai usato" inesistente. FAMILIES = [ ("btc/eth certificati 5m/15m/1h", "btc_*.parquet|eth_*.parquet", "btc_1h.parquet", ("load_data", "altlib")), ("alt_sol_* (SOL, escluso 22/08)", "alt_sol_*.parquet", "alt_sol_1h.parquet", ("alt_sol",)), ("hl_*_1d (51 alt Hyperliquid)", "hl_*_1d.parquet", "hl_btc_1d.parquet", ("hl_{",)), ("hlfund_*_1h (19 major)", "hlfund_*_1h.parquet", "hlfund_btc_1h.parquet", ("hlfund_{",)), ("eq_*_1d (29 ETF USA)", "eq_*_1d.parquet", "eq_spy_1d.parquet", ("eq_{", 'eq_*.parquet')), ("eqx_*_1d (12 UCITS)", "eqx_*_1d.parquet", "eqx_vuaa_1d.parquet", ("eqx",)), ("fut_*_1h (7 futures indice)", "fut_*_1h.parquet", "fut_es_1h.parquet", ("fut_{",)), ("fut_deribit/ (70 datati scaduti)", "fut_deribit/*-*.parquet", "fut_deribit/BTC-27DEC24.parquet", ("fut_deribit",)), ("fut_deribit/fund_* (funding 1h)", "fut_deribit/fund_*.parquet", "fut_deribit/fund_BTC.parquet", ("fut_deribit",)), ("dvol_* (vol implicita)", "dvol_*.parquet", "dvol_btc.parquet", ("dvol_{",)), ("vol_term_* (term structure)", "vol_term_*.parquet", "vol_term_btc.parquet", ("vol_term_{",)), ("fundnews_short_screen", "fundnews_*.parquet", "fundnews_short_screen.parquet", ("fundnews_",)), ("cb_chain/ (catena opzioni oraria)", "cb_chain/*.parquet", "cb_chain/bite_archive.parquet", ("cb_chain",)), ("cb_market_snapshots", "cb_market_snapshots.parquet", "cb_market_snapshots.parquet", ("cb_market_snapshots",)), ] # dataset a schema RICCO: qui si scende a livello di COLONNA RICH = [ ("cb_chain/bite_archive.parquet", RAW / "cb_chain" / "bite_archive.parquet"), ("cb_market_snapshots.parquet", RAW / "cb_market_snapshots.parquet"), ("vol_term_btc.parquet", RAW / "vol_term_btc.parquet"), ("hlfund_btc_1h.parquet", RAW / "hlfund_btc_1h.parquet"), ] NON_PARQUET = [ ("external/coinmetrics/cm_{btc,eth}.csv", DATA / "external/coinmetrics/cm_btc.csv", "cm_btc"), ("external/coinmetrics/fng.json", DATA / "external/coinmetrics/fng.json", "fng.json"), ("external/coinmetrics/stables.json", DATA / "external/coinmetrics/stables.json", "stables.json"), ("external/premium/{cb,upbit}_*.csv + usdkrw", DATA / "external/premium/cb_btc.csv", "usdkrw"), ("external/stable_snapshots/snapshots.jsonl", DATA / "external/stable_snapshots/snapshots.jsonl", "stable_snapshots"), ("external/hlp_deepdive/ + hlp_vault.json", DATA / "external/hlp_vault.json", "hlp_"), ("options_daily/snapshots.jsonl (0DTE)", DATA / "options_daily/snapshots.jsonl", "options_daily"), ("chain_collect/runs.jsonl (battito)", DATA / "chain_collect/runs.jsonl", "chain_collect"), ("instruments_registry.json", DATA / "instruments_registry.json", "instruments_registry"), ] def fase1() -> None: print(LINE) print(" FASE 1 — INVENTARIO: cosa c'e' su disco e CHI lo legge") print(LINE) idx = _index_repo() print(f" indice repo: {len(idx)} file .py in src/ scripts/ tests/ (Old/ escluso = archivio)\n") print(" (A) FAMIGLIE DI FILE") print(f" {'dataset':36s} {'file':>5s} {'righe*':>10s} {'intervallo':22s} lettori (script che aprono il path)") print(" " + "-" * 130) for label, pattern, sample, keys in FAMILIES: files: list[Path] = [] for pat in pattern.split("|"): files += sorted(RAW.glob(pat)) if not files: print(f" {label:36s} {'0':>5s} ASSENTE") continue n, cols, rng = _pq_info(RAW / sample) # lettori: chi cita il MODO in cui quei path si costruiscono (chiavi esplicite: derivarle # dall'etichetta dava 2 falsi "NESSUNO" su famiglie realmente lette). Colonna INDICATIVA: # i due fut_deribit condividono l'accessore (FUT_DIR) e non si distinguono. rd = sorted({f for k in keys for f in _readers(idx, k, word=False)}) rdl = ", ".join(Path(x).name for x in rd[:3]) + (f" (+{len(rd)-3})" if len(rd) > 3 else "") print(f" {label:36s} {len(files):5d} {n:10,d} {rng:22s} {rdl if rd else '*** NESSUNO ***'}") print(f" {'':36s} {'':5s} {'':10s} cols: {', '.join(cols[:12])}{' ...' if len(cols) > 12 else ''}") print(" * righe = del file CAMPIONE, non della famiglia intera.\n") print(" (B) COLONNE, dataset per dataset — 'MAI LETTA' = zero file in src/ scripts/ tests/") for label, p in RICH: if not p.exists(): continue n, cols, rng = _pq_info(p) print(f"\n --- {label} ({n:,} righe, {rng})") for c in cols: rd = _readers(idx, c) if not rd: print(f" {c:26s} *** MAI LETTA ***") else: print(f" {c:26s} {len(rd):3d} file ({', '.join(Path(x).name for x in rd[:3])})") print("\n --- external/coinmetrics/cm_{btc,eth}.csv (31 colonne, daily dal 2009/2015)") cm = pd.read_csv(DATA / "external/coinmetrics/cm_btc.csv", low_memory=False) mai, lette = [], [] for c in cm.columns: if c == "time": continue (lette if _readers(idx, c) else mai).append(c) print(f" LETTE ({len(lette)}): {', '.join(lette)}") print(f" *** MAI LETTE ({len(mai)}) ***: {', '.join(mai)}") print("\n (C) SORGENTI NON-PARQUET") for label, p, key in NON_PARQUET: if not p.exists(): print(f" {label:44s} ASSENTE") continue sz = p.stat().st_size rd = _readers(idx, key, word=False) print(f" {label:44s} {sz/1e6:7.2f} MB lettori: " f"{', '.join(Path(x).name for x in rd[:3]) if rd else '*** NESSUNO ***'}") print("\n (D) DIRECTORY DI STATO (non sono sorgenti di segnale — elencate per completezza)") for d in sorted(DATA.iterdir()): if not d.is_dir() or d.name in ("raw", "external"): continue nf = sum(1 for _ in d.rglob("*") if _.is_file()) sz = sum(f.stat().st_size for f in d.rglob("*") if f.is_file()) print(f" data/{d.name:22s} {nf:4d} file {sz/1e6:8.2f} MB") # =========================================================================== # FASE 2 — LA MISURA # =========================================================================== # ---- il dato mai letto ----------------------------------------------------- def _flows(asset: str) -> tuple[pd.Series, pd.Series]: """FlowInExNtv / FlowOutExNtv: monete che ENTRANO e che ESCONO dagli exchange, in unita' native, giornaliere. Zero file del repo le leggono (verificato in FASE 1).""" cm = ow._CM[asset] I = cm["FlowInExNtv"].astype(float) O = cm["FlowOutExNtv"].astype(float) return I.where(I > 0), O.where(O > 0) def _z(s: pd.Series, W: int) -> pd.Series: mp = max(20, W // 2) return (s - s.rolling(W, min_periods=mp).mean()) / s.rolling(W, min_periods=mp).std() # ---- le tre variabili (segno a priori dichiarato nel docstring) ------------ def gross_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): """GROSS — turnover lordo `log(In+Out)`, z-score causale su W. LA PARTE NUOVA: si cancella nella differenza, quindi lo STOCK gia' testato non la contiene. Segno a priori +1 (partecipazione alta = mercato vivo).""" def fn(df, asset): I, O = _flows(asset) z = _z(np.log(I + O), W) * sign pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) return ow._to_target(df, pos) return fn def imb_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): """IMB — squilibrio normalizzato `(Out-In)/(Out+In)`, z-score su W. Netto RISCALATO dal lordo: meta' informazione nuova, meta' del vecchio. Segno a priori +1 (deflussi = accumulo).""" def fn(df, asset): I, O = _flows(asset) z = _z((O - I) / (O + I), W) * sign pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) return ow._to_target(df, pos) return fn def netctl_factory(tf: str, W: int = 90, mode: str = "LF", sign: int = 1): """NETCTL — CONTROLLO NEGATIVO: deflusso netto cumulato su W diviso lo stock. E' la stessa informazione di `SplyExNtv`, cioe' della famiglia gia' UCCISA. Sta nella griglia apposta: se la selezione in-sample lo preferisce alle due variabili nuove, la risposta e' che il dataset non contiene nient'altro di selezionabile.""" def fn(df, asset): I, O = _flows(asset) S = ow._CM[asset]["SplyExNtv"].astype(float) S = S.where(S > 0) z = (((O - I).rolling(W, min_periods=max(20, W // 2)).sum()) / S) * sign pos = np.sign(z) if mode == "LS" else (z > 0).astype(float) return ow._to_target(df, pos) return fn VARS = {"GROSS": gross_factory, "IMB": imb_factory, "NETCTL": netctl_factory} # ---- GRIGLIA DICHIARATA PRIMA DI GUARDARE QUALSIASI SHARPE ---------------- WINDOWS = (30, 90, 180) MODES = ("LF", "LS") SIGNS = (1, -1) GRID_1VAR = [dict(W=w, mode=m, sign=s) for w in WINDOWS for m in MODES for s in SIGNS] # 12 GRID_FULL = [dict(var=v, **p) for v in VARS for p in GRID_1VAR] # 36 EXS_GRID_SPESA = [dict(L=30, mode="LF"), dict(L=90, mode="LF"), dict(L=30, mode="LS"), dict(L=90, mode="LS")] # 4, gia' spese 24/07 def family_factory(tf: str, var: str = "GROSS", **p): return VARS[var](tf=tf, **p) def _active_bars(fn, asset: str) -> tuple[int, int, int, int]: """barre con posizione != 0, FULL e HOLD-OUT. Il CSV CoinMetrics si ferma al 2026-05-24: dopo, posizione 0 per assenza di dato -> non e' evidenza, e non va contata come tale.""" df = al.get(asset, "1d") t = np.nan_to_num(np.asarray(al._call_target(fn, df, asset), float)) idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) act = np.abs(t) > 0 h = idx >= al.HOLDOUT return int(act.sum()), len(t), int((act & h).sum()), int(h.sum()) def fase2() -> None: print("\n" + LINE) print(" FASE 2 — EXFLOW: il turnover LORDO sugli exchange (colonne mai lette)") print(LINE) # ---------------------------------------------------------------- 0. perche' questa print(""" PERCHE' QUESTA E NON UN'ALTRA (i quattro criteri, applicati all'inventario): (i) storia — FlowIn/FlowOut coprono 2018->2026 al 100% su ENTRAMBI gli asset. Le altre colonne mai lette hanno finestre di mesi: `volume_24h` della catena (113 g), `book_depth_top3` (113 g), `options_daily` 0DTE (30 g), `iv_90d` (58 righe), `fetch_errors_json` (diagnostica, non segnale). (ii) meccanismo — il turnover lordo misura quanta parte del flottante viene ri-posizionata. (iii) eseguibile — segnale giornaliero -> long-flat/long-short BTC+ETH sui perp Deribit: esattamente l'impianto di TP01. Verificato sotto a $635 con min-order $5. (iv) non chiusa — le due colonne non sono lette da NESSUN file. Ma la FAMIGLIA (exchange-flow) e' stata uccisa il 24/07 sullo STOCK -> serve un meccanismo nuovo, misurato al blocco 1, e i trial vanno contati AL RIALZO (blocco 5).""") # ---------------------------------------------------------------- 1. il fatto strutturale print("\n" + "-" * 100) print(" 1. IL FATTO CHE GIUSTIFICA (o no) LA RIAPERTURA — si misura, non si asserisce") print("-" * 100) print(" EXS (24/07) ha testato SplyExNtv = lo STOCK. d(stock) = In - Out = il NETTO.") print(" Se corr(d stock, netto) ~ 1 e corr(d stock, lordo) ~ 0, allora il LORDO e'") print(" algebricamente assente da cio' che e' stato provato — ed e' l'unica parte nuova.\n") for a in ("BTC", "ETH"): I, O = _flows(a) S = ow._CM[a]["SplyExNtv"].astype(float).where(lambda s: s > 0) m = I.notna() & O.notna() & S.notna() dS, net, gross = S[m].diff(), (I - O)[m], (I + O)[m] ok = dS.notna() zg = _z(np.log(gross), 180) mom = np.log(S[m]) - np.log(S[m]).shift(30) ratio = (net.abs() / gross) big = gross[zg > 3] print(f" {a} n={int(m.sum()):,} {I[m].index[0].date()} -> {I[m].index[-1].date()}") print(f" corr( d(SplyEx) , In-Out ) = {dS[ok].corr(net[ok]):+.4f} <- il NETTO E' la derivata dello stock") print(f" corr( d(SplyEx) , In+Out ) = {dS[ok].corr(gross[ok]):+.4f} <- il LORDO no") print(f" corr( z(lordo) , momentum-30g dello stock ) = {zg.corr(mom):+.4f}") print(f" |In-Out| / (In+Out) mediano = {float(ratio.median()):.3f} -> il {100*(1-float(ratio.median())):.0f}% del") print(" flusso lordo SI CANCELLA: lo stock vede sei monete su cento fra quelle che si muovono.") if len(big): print(f" (nei {len(big)} giorni a z(lordo)>3 il rapporto resta {float(ratio[zg > 3].median()):.3f})") # ---------------------------------------------------------------- 2. griglia dichiarata print("\n" + "-" * 100) print(" 2. GRIGLIA DICHIARATA PRIMA DI GUARDARE, E CONTATA AL RIALZO") print("-" * 100) print(f" variabili {tuple(VARS)} x W {WINDOWS} x mode {MODES} x sign {SIGNS}") print(f" = {len(GRID_FULL)} celle nuove, tf 1d soltanto.") print(f" + {len(EXS_GRID_SPESA)} celle GIA' SPESE su questa famiglia il 24/07 (EXS) = {len(GRID_FULL)+len(EXS_GRID_SPESA)} trial") print(" 'quando si riapre un parametro si riapre la sua famiglia' (regola 2026-07-30).") # ---------------------------------------------------------------- 3. controllo positivo print("\n" + "-" * 100) print(" 3. CONTROLLO POSITIVO — la famiglia UCCISA, ri-girata con la SUA factory") print("-" * 100) rep_exs = study_family_honest("EXS-replica(24/07)", ow.exs_factory, EXS_GRID_SPESA, tfs=("1d",)) ch = rep_exs["chosen"] print(f" cella IS {ch['params']} IS {ch['insample_sharpe']} FULL {ch['full_sharpe']} " f"DSR {rep_exs['deflated_sharpe']} earns_slot_honest={rep_exs['earns_slot_honest']}") print(" atteso: False (l'ondata 24/07 chiuse 0/6). Se qui uscisse True, il macchinario") print(" non sarebbe credibile sul caso nuovo e il resto andrebbe buttato.") exs_sharpes = [r["full_sharpe"] for r in rep_exs["rows"]] # ---------------------------------------------------------------- 4. il gate di famiglia print("\n" + "-" * 100) print(" 4. GATE DI FAMIGLIA — `study_family_honest` (cella scelta IN-SAMPLE, DSR, marginal)") print("-" * 100) rep = study_family_honest("EXFLOW", family_factory, GRID_FULL, tfs=("1d",)) ch = rep["chosen"] daily = al.candidate_daily(family_factory(tf="1d", **ch["params"]), tf="1d") hold = daily[daily.index >= al.HOLDOUT] print(f" celle valutate: {rep['n_cells']}") print(f" cella scelta AL BUIO (solo in-sample): {ch['params']}") print(f" Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']} HOLD-OUT {al._sh(hold):.3f}") print(f" deflated-Sharpe {rep['deflated_sharpe']} (massimo atteso dal PURO RUMORE: " f"Sharpe {rep['expected_null_max']}) pass={rep['dsr_pass']}") print(fmt_marginal(rep["marginal"])) print(f" >>> earns_slot_honest = {rep['earns_slot_honest']}") print("\n classifica per Sharpe IN-SAMPLE (le prime 6 e le ultime 3 di 36):") for r in rep["rows"][:6] + rep["rows"][-3:]: d = al.candidate_daily(family_factory(tf="1d", **r["params"]), tf="1d") print(f" {str(r['params']):58s} IS {r['insample_sharpe']:6.3f} FULL {r['full_sharpe']:6.3f}" f" HOLD {al._sh(d[d.index >= al.HOLDOUT]):6.3f}") # ---------------------------------------------------------------- 5. sensibilita' al conteggio print("\n" + "-" * 100) print(" 5. SENSIBILITA' DEL VERDETTO AL CONTEGGIO DEI TRIAL (regola 2026-07-30)") print("-" * 100) new_sh = [r["full_sharpe"] for r in rep["rows"]] # ⚠️ ERRORE MIO, CORRETTO PRIMA DI PUBBLICARE: la prima stesura usava `new_sh[:12]` come # "una sola variabile". `rows` e' ORDINATA per Sharpe in-sample -> quelle 12 erano le 12 # MIGLIORI, un pool a varianza artificialmente bassa, e il DSR usciva 0.975 = PASS. Non e' # una lettura generosa, e' un pool sbagliato. La partizione legittima e' la variabile scelta. own = [r["full_sharpe"] for r in rep["rows"] if r["params"]["var"] == ch["params"]["var"]] for lab, pool in ((f"N=12 (la sola variabile scelta, {ch['params']['var']})", own), ("N=36 (griglia nuova dichiarata)", new_sh), ("N=40 (+ le 4 gia' spese su EXS il 24/07)", new_sh + exs_sharpes)): dsr, sr0 = deflated_sharpe(al._sh(daily), pool, daily) print(f" {lab:48s} DSR {dsr:.3f} null-max {sr0:.3f} (n pool {len(pool)})") print(" Il verdetto e' lo stesso a ogni conteggio LEGITTIMO. Ma il DSR resta sensibile a") print(" COME si partiziona la griglia (lezione §10 dell'ondata): con un pool scelto male") print(" — le 12 celle migliori — lo stesso candidato PASSA a 0.975. La partizione e' il") print(" secondo posto dove barare e' indolore e invisibile.") # ---------------------------------------------------------------- 6. sotto-famiglie print("\n" + "-" * 100) print(" 6. DECOMPOSIZIONE PER VARIABILE (diagnostica: riportare il meglio di tre E' selezione,") print(" quindi il numero che vale resta quello del blocco 4)") print("-" * 100) for nm, fac in VARS.items(): r = study_family_honest(nm, fac, GRID_1VAR, tfs=("1d",)) c = r["chosen"] d = al.candidate_daily(fac(tf="1d", **c["params"]), tf="1d") print(f" {nm:7s} IS-pick {str(c['params']):40s} IS {c['insample_sharpe']:6.3f} " f"FULL {c['full_sharpe']:6.3f} HOLD {al._sh(d[d.index >= al.HOLDOUT]):6.3f} " f"DSR(12) {r['deflated_sharpe']} marg {r['marginal']['marginal_verdict']} " f"corr->TP01 {r['marginal']['marginal']['corr_full']}") print(" La selezione in-sample sull'INTERA griglia preferisce NETCTL, cioe' il CONTROLLO") print(" NEGATIVO = l'informazione gia' uccisa. La parte nuova non e' selezionabile.") # ---------------------------------------------------------------- 7. direzione o volatilita' print("\n" + "-" * 100) print(" 7. DOVE STA L'INFORMAZIONE — direzione o volatilita'? (la letteratura che il progetto") print(" stesso cita il 24/07 dice: i flussi predicono la VOL, non la direzione)") print("-" * 100) def _t(x, y): c = float(np.corrcoef(x, y)[0, 1]) return c, c * np.sqrt(len(x) - 2) / np.sqrt(max(1e-12, 1 - c * c)) for a in ("BTC", "ETH"): df = al.get(a, "1d") idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D") r = pd.Series(al.simple_returns(df["close"].values.astype(float)), index=idx) I, O = _flows(a) zg = _z(np.log(I + O), 180).shift(ow.AVAIL_LAG_D).reindex(idx) zi = _z((O - I) / (O + I), 180).shift(ow.AVAIL_LAG_D).reindex(idx) rv = r.rolling(30).std() fdir, fabs = r.shift(-1), r.abs().shift(-1) m = zg.notna() & zi.notna() & fdir.notna() & rv.notna() n = int(m.sum()) cd, td = _t(zg[m], fdir[m]) ci, ti = _t(zi[m], fdir[m]) ca, ta = _t(zg[m], fabs[m]) cb, tb = _t(rv[m], fabs[m]) # incrementale ONESTO: residuo OLS di |r_{t+1}| su RV30_t, poi correlato con z(lordo) X = np.column_stack([np.ones(n), rv[m].values]) beta = np.linalg.lstsq(X, fabs[m].values, rcond=None)[0] resid = fabs[m].values - X @ beta cr, tr = _t(zg[m].values, resid) rho, pv = stats.spearmanr(zg[m].values, resid) yr_idx = zg[m].index per_yr = {int(y): round(float(np.corrcoef(zg[m].values[yr_idx.year == y], resid[yr_idx.year == y])[0, 1]), 3) for y in sorted(set(yr_idx.year))} print(f" {a} n={n}") print(f" DIREZIONE corr(z lordo, r_domani) = {cd:+.4f} t={td:+5.2f}") print(f" corr(z squilibrio, r_dom) = {ci:+.4f} t={ti:+5.2f}") print(f" VOLATILITA corr(z lordo, |r|_domani) = {ca:+.4f} t={ta:+5.2f}") print(f" baseline corr(RV30, |r|) = {cb:+.4f} t={tb:+5.2f} <- il confronto giusto") print(f" z lordo sul RESIDUO OLS = {cr:+.4f} t={tr:+5.2f} <- l'incremento vero") print(f" lo stesso in RANGO (Spearman) = {rho:+.4f} p={pv:.4f} <- e' monotono o e' coda?") print(f" Pearson per anno: {per_yr}") # ---------------------------------------------------------------- 8. gate residui print("\n" + "-" * 100) print(" 8. GATE RESIDUI SULLA CELLA SCELTA") print("-" * 100) fn = family_factory(tf="1d", **ch["params"]) print(f" causality_ok : {causality_ok(fn, tf='1d')}") imp = implausible_sharpe(daily) print(f" implausible_sharpe: implausible={imp['implausible']} reasons={imp['reasons']} " f"attive={imp['n_active']} quota-perdite-su-attive={imp['loss_frac']:.3f}") print(" eseguibilita' a $635 (min-order $5):") for a in ("BTC", "ETH"): df = al.get(a, "1d") sc = eval_weights_smallcap(df, al._call_target(fn, df, a), capital=635.0, min_order=5.0) af, tf_, ah, th = _active_bars(fn, a) print(f" {a}: haircut Sharpe {sc['sharpe_haircut']:+.3f} trade eseguiti {sc['n_executed_trades']:4d}" f" | barre ATTIVE {af}/{tf_} ({100*af/tf_:.1f}%) hold-out attive {ah}/{th}") # ---------------------------------------------------------------- 9. potenza print("\n" + "-" * 100) print(" 9. POTENZA — in barre ATTIVE, non di calendario") print("-" * 100) tgt = np.abs(np.nan_to_num(np.asarray(al._call_target(fn, al.get('BTC', '1d'), 'BTC'), float))) > 0 print(f" il CSV CoinMetrics si ferma al {ow._CM['BTC'].index[-1].date()}: dopo quella data la") print(" posizione e' FLAT per ASSENZA DI DATO. Contarlo come evidenza sarebbe l'errore gia'") print(" pagato dal progetto (zeri letti come 'nessuna perdita').") for lab, s in (("FULL", daily), ("HOLD-OUT", hold)): nz = int((s != 0).sum()) yr = nz / 365.25 se = np.sqrt(1.0 / max(yr, 1e-9)) print(f" {lab:9s} barre non-nulle {nz:5d} ({yr:.2f} anni) SE(Sharpe) ~ {se:.2f}" f" -> differenza rilevabile al 95% ~ {1.96*se:.2f} di Sharpe") print(f" (usato solo per dire cosa la finestra puo' e non puo' decidere: n_target_attivo={int(tgt.sum())})") # ---------------------------------------------------------------- 10. verdetto print("\n" + LINE) print(" VERDETTO") print(LINE) print(f""" SCARTATO, per tre ragioni indipendenti e in quest'ordine di forza: (1) LA CELLA SCELTA AL BUIO STA SOTTO IL RUMORE. Sharpe FULL {ch['full_sharpe']} contro un massimo atteso dal PURO RUMORE di {rep['expected_null_max']} su {rep['n_cells']} trial. Non e' "non passa il gate per poco": e' sotto la soglia che una griglia di monete raggiungerebbe da sola. Stessa aritmetica di ORTHO-SCREEN (§12). (2) LA SELEZIONE IN-SAMPLE TORNA SUL CONTROLLO NEGATIVO. Fra le tre variabili sceglie NETCTL, cioe' l'informazione dello STOCK gia' uccisa il 24/07. Il turnover LORDO — l'unica parte algebricamente nuova (corr con d(stock) 0.03-0.08, e il 94% del flusso che si cancella) — NON e' selezionabile: la sua migliore cella in-sample fa hold-out NEGATIVO (-0.42) ed e' DILUTES contro TP01. (3) ZERO INFORMAZIONE DIREZIONALE, E IL LEGAME CON LA VOL NON REGGE AL SECONDO SGUARDO. corr(segnale, ritorno di domani): |t| < 1 su 2 asset x 2 variabili nuove — ed e' la direzione cio' di cui una strategia aveva bisogno. Sul |ritorno| il quadro sembra diverso — ETH da' Pearson incrementale sul residuo di RV30 t=+2.70 — ma: * in RANGO (Spearman) diventa +0.015 con p=0.46 -> non e' una relazione monotona, e' la coda della distribuzione dei livelli; * per anno decade e cambia segno (2021 +0.13 ... 2024 -0.03, 2026 -0.06); * su BTC lo stesso test da' -0.005 (Pearson) e -0.051 con p=0.007 (Spearman), cioe' "significativo" nel verso SBAGLIATO = firma di rumore, non di segnale. Il turnover sugli exchange sale quando la vol e' GIA' salita: termometro contemporaneo, stessa forma gia' trovata su TERM-STRUCTURE (§7) e SKEW (§5). ⚠️ E anche se il legame fosse reale, non produrrebbe un candidato: l'unica espressione eseguibile di una vista sulla VOL a $635 e' short-vol (VRP01: 4/4 gate falliti, 5/5 overlay refutati, regola "niente short-vol da modello in deploy") oppure il denominatore del vol-target di TP01 (TP01xDVOL, refutato come de-levering il 26/06). E' chiuso a valle, non solo a monte. COSA SI PORTA A CASA COMUNQUE: * il marginal scorer da' corr->TP01 0.50-0.65 su colonne che l'ondata 24/07 non aveva MAI letto -> REPLICA INDIPENDENTE del suo finding ("l'on-chain tradabile e' prezzo travestito, corr 0.50-0.82"), raggiunta da un'altra porta e con un altro stimatore; * l'eseguibilita' a $635 non ha bocciato nulla (haircut 0.000 su entrambe le gambe): ennesima conferma che il vincolo del progetto non e' piu' il capitale ma l'edge; * il pezzo di METODO riusabile e' il CONTROLLO NEGATIVO DENTRO LA GRIGLIA: infilare nella stessa famiglia una variabile che si SA gia' morta trasforma "il candidato perde" in "il dato non contiene nient'altro di selezionabile", che e' un'affermazione molto piu' forte e costa una factory. RISPOSTA ALLA DOMANDA DEL FILONE: dei dati che nessuno legge, uno solo aveva storia sufficiente per sostenere un'ipotesi, ed e' stato misurato fino in fondo. Tutto il resto dell'inventario e' telemetria a finestra corta (30-113 giorni) o gia' analizzato. **Nessun dato inutilizzato sostiene oggi un'ipotesi nuova.** """) def main() -> None: skip = "--skip-inventory" in sys.argv print(LINE) print(" §43 DATA-UNUSED — quali dati il progetto possiede e nessuna strategia legge") print(f" repo {ROOT} (sola lettura: nessuna scrittura, nessun ordine)") print(LINE) if not skip: fase1() fase2() if __name__ == "__main__": main()