Files
Adriano Dal Pastro 696f2e5bdd research(§43 DATA-UNUSED): inventario dei dati mai letti + l'unica ipotesi che ne esce, misurata
FASE 1 — censimento di data/ e di CHI lo legge (indice di 647 .py, Old/ escluso).
Colonne MAI lette: 26 su 31 di CoinMetrics (fra cui FlowIn/FlowOutExNtv, 2018-2026 al 100%),
`fetch_errors_json`, `iv_90d`. Tutto il resto con storia vera e' gia' stato analizzato: cio'
che resta non letto e' telemetria a finestra corta (catena opzioni 113 g, 0DTE 30 g, vol_term
58 righe). Corretti in sessione due difetti dell'inventario stesso: la ricerca a SOTTOSTRINGA
dava `iv` in 571 file, e le chiavi derivate dall'etichetta producevano due falsi "NESSUNO"
su famiglie realmente lette (eqx_, fut_deribit/fund_).

FASE 2 — EXFLOW: il turnover LORDO sugli exchange. SCARTATO.
La famiglia exchange-flow fu uccisa il 24/07 sullo STOCK (SplyExNtv). Misurato prima di
riaprirla: `In-Out` e' la derivata dello stock (corr +0.999 BTC / +0.752 ETH) mentre `In+Out`
e' ortogonale (+0.035 / +0.080) — il 94% del flusso si cancella, quindi il lordo e'
algebricamente assente da cio' che era stato provato. Griglia dichiarata prima: 3 variabili x
3 finestre x 2 modi x 2 segni = 36 celle, +4 gia' spese su EXS = 40 trial.
(1) La cella scelta al buio fa Sharpe FULL 0.951 contro un massimo atteso dal PURO RUMORE di
1.009: sotto la soglia che una griglia di monete raggiunge da sola (DSR 0.437 a N=36, 0.411 a
N=40). (2) La selezione in-sample torna sul CONTROLLO NEGATIVO messo apposta nella griglia
(NETCTL = l'informazione gia' uccisa): il lordo non e' selezionabile, hold-out -0.42, DILUTES.
(3) Zero informazione direzionale (|t|<1 su 2 asset x 2 variabili). Il legame col |ritorno| su
ETH (Pearson incrementale t +2.70) non sopravvive al rango (Spearman p=0.46) e cambia segno per
anno; su BTC e' "significativo" nel verso sbagliato.
Sottoprodotti: corr->TP01 0.50-0.65 = replica indipendente del "l'on-chain e' prezzo travestito"
del 24/07 su colonne che quell'ondata non aveva letto; haircut 0.000 a $635.

Corretti prima di pubblicare due errori miei: il pool "N=12" del deflated-Sharpe erano le 12
celle MIGLIORI (rows e' ordinata) e faceva PASSARE il candidato a 0.975 — con la partizione
legittima fa 0.678; e il residuo di vol calcolato a mano invece che per OLS ribaltava il segno
su ETH.

Solo ricerca: nessuna scrittura, nessun ordine, book/pesi/cron/config INVARIATI.
2026-08-23 01:10:52 +00:00

595 lines
33 KiB
Python

"""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"(?<![A-Za-z0-9_]){re.escape(needle)}(?![A-Za-z0-9_])") if word else None
out = [f for f, t in idx.items() if (pat.search(t) if word else needle in t)]
if exclude_self:
out = [f for f in out if "r0823_data_unused" not in f]
return sorted(out)
def _pq_info(p: Path) -> tuple[int, list[str], str]:
try:
d = pd.read_parquet(p)
except Exception as e: # noqa: BLE001
return 0, [f"<illeggibile: {type(e).__name__}>"], ""
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()