diff --git a/scripts/research/r0823_data_unused.py b/scripts/research/r0823_data_unused.py new file mode 100644 index 0000000..53544c4 --- /dev/null +++ b/scripts/research/r0823_data_unused.py @@ -0,0 +1,594 @@ +"""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()