From aaddd2c93c7997c4d23cdda57fb45d3214207980 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 23 Aug 2026 02:55:39 +0000 Subject: [PATCH] =?UTF-8?q?research(prevday):=20=C2=A754=20GATE=20PREVDAY-?= =?UTF-8?q?01=20scritto=20=E2=80=94=20il=20lead=20piu'=20forte=20gira=20la?= =?UTF-8?q?=20cella=20SBAGLIATA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chiude un buco di PROCESSO: PREVDAY sta in forward-monitor dal 2026-06-21 senza gate pre-registrato e senza deflated-Sharpe, mentre XSR01/DVOLSPREAD/STATARB ne hanno uno. FAMIGLIA DICHIARATA PRIMA E CONTATA AL RIALZO: anchor{1,2,3,5} x k{0..1.00, 7} x short{T,F} x min_hold{0,24,72} x tf{1h,4h} = 336 celle (le 11 gia' spese in giugno da prevday_turnover e le >=8 della scoperta sono tutte DENTRO). Screen dichiarato: i 16 agenti dell'onda intraday + le 104 ipotesi del 20/06. IL RISULTATO PRINCIPALE — la cella scelta AL BUIO (in-sample-only) NON e' quella che gira: e' 4h LONG-FLAT (short=False), mentre il monitor gira 1h LONG-SHORT, che sta al rango 186/336 in-sample e FALLISCE il DSR (0.905 contro 0.993 della cella al buio). La gamba SHORT — l'intera ragione per cui PREVDAY fu promosso a LEAD — e' esattamente cio' che la selezione onesta non compra. E il rovescio: la cella al buio sta a corr 0.641 da TP01 contro 0.152 della congelata (diversifica di meno). BANDA D'ANCORA: PREVDAY UN'ANCORA CE L'HA (l'ora del confine-giorno) e §50 l'ha tenuta FERMA. De-luckata sullo spazio congiunto (TP01 x24 · SKH01 x23 · PREVDAY x24, 300 estrazioni, differenze APPAIATE): dShFULL +0.191 -> +0.100, dShHOLD +0.362 -> +0.246 al peso 15% — meta'. Il SEGNO regge al 100% delle estrazioni; la TAGLIA no. Replica indipendente di §50 a candidato fermo: +0.191/+0.362 contro i +0.192/+0.363 pubblicati. Standalone: canonica al 96o pctl delle 24 ancore (ShFULL 1.236 contro mediana 0.963). MDE: 63 giorni = 0.17 anni -> SE(Sharpe) 2.41 naive / 4.23 Lo, t 0.85 / 0.48, IC95 largo 16.6 punti. Per distinguere uno Sharpe vero di 1.2 dal nulla all'80% di potenza servono 9.4 anni. Il forward serve a UCCIDERE, non a promuovere. E il "+2,04" pubblicato e' su lente ORARIA: sulla lente giornaliera del progetto e' +1.56. DSR: attesa a priori REFUTATA. Non si ribalta col conteggio (11 -> 560 celle costa 0.009); si ribalterebbe solo con sd(trial) >= 0.363 contro 0.267 misurata. Su famiglia OMOGENEA il deflated-Sharpe e' cieco sul conteggio ma NON vacuo: la cella mediana della stessa famiglia fallisce (0.863). INTEGRITA' (sola lettura provata con md5+mtime): 1512 barre su 1512 ore, 95.6% ricostruibili bit-a-bit; le 67 divergenti sono 1.06/giorno all'ora del cron = ~32 min/giorno non registrati (contro i 4 min/GIORNO REGISTRATI di paper_statarb, §32). paper_prevday e' sano, ma non al 100%. FUNDING (mai in nessun backtest): -1.37%/anno di sleeve. La gamba short NON compensa — incassa solo 0.26-0.46x l'incondizionato mentre la lunga paga 1.25-1.69x. weights_tilt_null PASS a ogni peso, ma frac_random_beat_hold = 0.91: "migliora l'hold-out" e' un claim generico su questo libro. GATE PREVDAY-01: decisione 2027-06-21, 7 condizioni (DSR>=0.95 sulla famiglia ri-dichiarata + sensibilita' alla partizione pubblicata; cella al buio == congelata, altrimenti si ri-congela e il forward RIPARTE DA ZERO; delta di libro appaiato > +0.05 positivo al >=90%; weights_tilt_null; ADDS non-hedge sulla cella al buio; day_boundary_robust != ARTIFACT-RISK; Sharpe forward > 0, soglia debole di proposito), veto d'integrita' >=80% di barre ricostruibili, kill a Sharpe < -0.50 su >=180 giorni attivi. Oggi 8/10 condizioni; le 2 che mancano sono la stessa cosa. Nessun file di produzione toccato, nessuna proposta di cambio al libro live. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf --- scripts/research/r0823_prevday_gate.py | 1207 ++++++++++++++++++++++++ 1 file changed, 1207 insertions(+) create mode 100644 scripts/research/r0823_prevday_gate.py diff --git a/scripts/research/r0823_prevday_gate.py b/scripts/research/r0823_prevday_gate.py new file mode 100644 index 0000000..d99d72c --- /dev/null +++ b/scripts/research/r0823_prevday_gate.py @@ -0,0 +1,1207 @@ +#!/usr/bin/env python +"""r0823_prevday_gate.py — §54 PREVDAY-GATE: il lead PREVDAY ha un gate pre-registrato? + +IL BUCO CHE CHIUDE (di PROCESSO, non di misura). Il filone §50 (BOOK-3RD) ha misurato tutti i +candidati a terzo sleeve del LIBRO LIVE e ha trovato che PREVDAY e' il piu' forte (dSharpe di +libro +0,192 FULL / +0,363 HOLD al peso 15%, positivo nel 100% di 1000 estrazioni congiunte), +eseguibile, nettante col libro, ADDS, robust_oos, forward Sharpe +2,04 su 63 giorni — E STA IN +FORWARD-MONITOR DA GIUGNO SENZA GATE PRE-REGISTRATO E SENZA DEFLATED-SHARPE. Ogni altro lead del +progetto ne ha uno (XSR01 23/10, DVOLSPREAD 24/10, STATARB 27/09). Questo script NON e' un +mandato a promuovere: e' un mandato a GIUDICARE, con la stessa asticella degli altri tre. + +COSA FA (nell'ordine in cui va letto) + 0. ATTESA A PRIORI, scritta PRIMA di misurare (e poi confrontata con l'esito). + 1. LA FAMIGLIA dichiarata e i trial contati AL RIALZO — stampati PRIMA di ogni risultato. + 2. INTEGRITA' DEL MONITOR (sola lettura): `paper_prevday` e' davvero l'unico monitor sano? + Replay offline dell'aritmetica di `advance()` sulle barre certificate di OGGI e confronto + bit-a-bit con cio' che ha registrato. + 3. MDE della finestra forward: 63 giorni a Sharpe +2,04 — quanto e' distinguibile da zero? + 4. `study_family_honest` (componenti REALI di altlib): selezione in-sample-only -> + deflated_sharpe -> marginal_vs_tp01. La cella scelta al buio e' quella che gira? + 5. DSR: sensibilita' del verdetto al CONTEGGIO e alla PARTIZIONE (lezione 22/08: lo stesso + insieme di celle da' verdetti opposti secondo come lo si spezza). + 6. `day_boundary_robust` a +-2/+-4/+-8 ore, piu' la griglia oraria piena. + 7. BANDA D'ANCORA: standalone sui 24 confini-giorno, e delta di LIBRO su estrazioni + CONGIUNTE (TP01 x24 · SKH01 x23 · PREVDAY x24), mediana delle DIFFERENZE APPAIATE. + 8. FUNDING condizionale (r0822d_funding importato): un long-short paga o incassa? + 9. `weights_tilt_null` al peso proposto, con `frac_random_beat_hold`. + 10. IL GATE PRE-REGISTRATO: data, soglie, veto d'integrita', criterio di kill. + +================================================================================================= +ATTESA A PRIORI — scritta PRIMA di far girare qualunque cosa (2026-08-23) +================================================================================================= +A1. La cella scelta AL BUIO (in-sample-only) NON sara' quella congelata nel monitor. + Motivo: il docstring della scoperta (agent_09, 2026-06-21) dichiara in chiaro il criterio con + cui BUFFER_K=0.30 fu scelto — «A small/zero buffer ... on BTC's choppy 2025-26 HOLD-OUT it + whipsaws to a NEGATIVE hold-out. Widening the break to 30% ... flips the BTC HOLD-OUT + positive». E' selezione-sull'hold-out, scritta dall'autore stesso. Mi aspetto quindi che + in-sample-only esca un k PIU' PICCOLO. [se sbaglio, e' un punto A FAVORE di PREVDAY] +A2. DSR: con ~336 trial dichiarati e uno Sharpe standalone ~1,2 mi aspetto un PASS FRAGILE di + famiglia (0,95-0,99) e un FAIL con l'sr0 di SCREEN. Mi aspetto cioe' che il verdetto si + RIBALTI col conteggio — come il 30/07 su VRP01 (0,983 a N=8 / 0,948 a N=72). +A3. `day_boundary_robust`: il leaderboard di giugno dice ROBUST con spread 0,196 — NON INVARIANT. + Mi aspetto che resti positivo a tutti gli offset (non e' un artefatto di etichettatura) ma + con una banda larga, cioe' che l'ancora canonica (mezzanotte UTC) sia FORTUNATA. +A4. Banda d'ancora: in 4 audit su 4 (TP01, XS01, SKH01, e il libro) l'ancora canonica e' risultata + fortunata. Mi aspetto lo stesso qui, e quindi che il +0,192/+0,363 di §50 — misurato a + PREVDAY FERMO sul suo confine canonico — sia una SOVRASTIMA. §50 ha de-luckato TP01 e SKH01 + e NON il candidato: e' esattamente la lezione del 26/07 («se si de-lucka una strategia va + de-luckato anche il suo DELTA») applicata a se stessa. +A5. Funding: PREVDAY e' long-short, quindi la gamba short INCASSA. Ma e' un breakout-follow, e il + 22/08 ha misurato che i breakout entrano quando il funding e' caro (SKH01 2,41-3,44x + l'incondizionato). Attesa: drag netto POSITIVO ma piu' piccolo di quello di TP01 per unita' di + esposizione, fra 0 e 2%/anno di sleeve. +A6. `weights_tilt_null`: mi aspetto `frac_random_beat_hold` alto (0,8-0,95) come in §50 — cioe' + che «migliora l'hold-out» sia un claim GENERICO su questo libro. +A7. MDE: 63 giorni = 0,17 anni -> SE(Sharpe annualizzato) ~ 2,4. Mi aspetto che +2,04 NON sia + distinguibile da zero (t < 1) e che nessuna soglia sul Sharpe forward a questo orizzonte + abbia potenza. Se e' cosi', il gate NON puo' poggiare sulla finestra forward. + +================================================================================================= +VINCOLI RISPETTATI: nessun file di produzione toccato (src/, config/, scripts/live/, cron_*.sh, +tests/, data/paper_*, data/live sono in SOLA LETTURA — verificato a runtime con md5+mtime). +NESSUN ordine. Rete: solo l'aggiornamento incrementale della cache funding (GET pubbliche +Deribit, <=2 req/s, con astensione ai minuti del cron), disattivabile con --no-net. + +USO + nice -n 19 timeout 900 uv run python scripts/research/r0823_prevday_gate.py + ... --no-net usa solo la cache funding + ... --draws N estrazioni d'ancora congiunte (default 300) + ... --fast griglia ridotta (solo per sviluppo; NON e' il conteggio dichiarato) +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import math +import pickle +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd +from scipy.stats import norm + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (str(ROOT), str(ROOT / "scripts" / "research"), + str(ROOT / "scripts" / "research" / "alt")): + if _p not in sys.path: + sys.path.insert(0, _p) + +# --- produzione (IMPORTATA, mai riscritta) --------------------------------------------------- +from src.portfolio.portfolio import (HOLDOUT, combine_outer, metrics, # noqa: E402 + to_daily, weights_tilt_null) +from src.live import book as LB # noqa: E402 +from src.strategies import prevday_breakout as pb # noqa: E402 + +# --- macchineria di ricerca gia' validata (riusata, non riscritta) --------------------------- +import altlib as al # noqa: E402 +import r0822d_funding as FU # noqa: E402 + +# ============================================================================================= +# COSTANTI E SOGLIE — DICHIARATE PRIMA DI MISURARE +# ============================================================================================= +ASSETS = ("BTC", "ETH") +FEE_SIDE = al.FEE_SIDE # 0,05%/lato = il default del progetto +W_TP, W_SKH = LB.W_TP01, LB.W_SKH # 0,75 / 0,25 LETTI dalla produzione +BOOK_START = pd.Timestamp("2019-03-14", tz="UTC") +TP_HOURS = tuple(range(24)) +SKH_OFFSETS = tuple(range(0, 690, 30)) +PV_BOUNDARY = tuple(range(24)) # l'ancora di PREVDAY: l'ora del confine-giorno +SEED = 20260823 + +# Il peso proposto e' DICHIARATO PRIMA di guardare la banda: 15% = lo stesso peso con cui il +# progetto ha valutato XSR01 e DVOLSPREAD come terzi candidati. Non e' scelto sul risultato. +W_PROPOSTO = 0.15 +WEIGHTS_TESTED = (0.05, 0.10, 0.15, 0.20) + +# --- SOGLIE DEL GATE, dichiarate qui, prima di ogni misura ----------------------------------- +GATE_DSR_MIN = 0.95 # standard del progetto (Bailey & Lopez de Prado, PASS >= 0,95) +GATE_ANCHOR_DELTA_MIN = 0.05 # stessa soglia di GATE PROP-01 gamba (b), 22/08 +GATE_ANCHOR_FRAC_MIN = 0.90 # il delta dev'essere positivo in >= 90% delle estrazioni +GATE_INTEGRITY_MIN = 0.80 # quota di barre forward ricostruibili (stessa soglia del veto + # d'integrita' di DVOLSPREAD, riusata perche' i gate restino + # confrontabili fra loro) +KILL_SHARPE = -0.50 # stessa soglia di kill di DVOLSPREAD + +SCRATCH = ROOT.parent / "_nonesistente" +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pvgate") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_pvgate") +B3RD_CACHE = SCRATCH.parent / "b3rd" / "series_v3.pkl" +PAPER_DIR = ROOT / "data" / "paper_prevday" +LEADERBOARD = ROOT / "scripts" / "research" / "intraday" / "intra_leaderboard.json" + +_T0 = time.time() + + +def clock() -> str: + return f"[{time.time()-_T0:6.1f}s]" + + +def sez(t: str) -> None: + print("\n" + "=" * 97) + print(f" {t}") + print("=" * 97) + + +def sub(t: str) -> None: + print(f"\n --- {t} " + "-" * max(0, 88 - len(t))) + + +# ============================================================================================= +# LA FAMIGLIA — gradi di liberta' del segnale +# ============================================================================================= +# ANCHOR_DAYS : su quanti giorni UTC precedenti si misura il range che definisce il livello +# BUFFER_K : quanto oltre il livello dev'essere la rottura per essere "decisiva" +# ALLOW_SHORT : libro simmetrico (rottura al ribasso -> short) o long-flat +# MIN_HOLD_H : soppressione dei flip entro N ore dall'ultimo (leva sul turnover) +# TF : la barra su cui il segnale valuta la rottura +G_ANCHOR = (1, 2, 3, 5) +G_K = (0.00, 0.10, 0.20, 0.30, 0.50, 0.75, 1.00) +G_SHORT = (True, False) +G_MH = (0, 24, 72) +G_TF = ("1h", "4h") + +FROZEN = dict(anchor=pb.ANCHOR_DAYS, k=pb.BUFFER_K, short=pb.ALLOW_SHORT, mh=0) +FROZEN_TF = "1h" + + +def _min_hold(direction: np.ndarray, min_hold_bars: int) -> np.ndarray: + """Sopprime i flip entro min_hold_bars dall'ultimo cambio di segno. + Copia ESATTA di scripts/research/intraday/prevday_turnover._min_hold (giugno 2026).""" + if min_hold_bars <= 0: + return direction + out = direction.copy() + last_flip = -10 ** 9 + cur = out[0] + for i in range(len(out)): + if np.sign(out[i]) != np.sign(cur): + if i - last_flip >= min_hold_bars: + cur = out[i] + last_flip = i + else: + out[i] = cur + else: + cur = out[i] + return out + + +def factory(tf: str, anchor: int, k: float, short: bool, mh: int): + """factory(tf=..., **params) -> target_fn(df), nella forma che select_cell_insample vuole. + I parametri di VOL-TARGET restano quelli congelati: la famiglia riaperta e' quella del + SEGNALE (livello, soglia, direzione, holding, TF), non quella del sizing.""" + def fn(df: pd.DataFrame) -> np.ndarray: + d = pb._breakout_direction(df, anchor, k, short) + if mh: + bars = max(1, int(round(mh * al.bars_per_day(df) / 24.0))) + d = _min_hold(d, bars) + pos = pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP) + return np.nan_to_num(pos, nan=0.0) + return fn + + +def frozen_fn(): + return factory(tf=FROZEN_TF, **FROZEN) + + +# ============================================================================================= +# 0) ATTESA A PRIORI + 1) LA FAMIGLIA +# ============================================================================================= +def sezione0_1(grid: list, tfs: tuple) -> dict: + sez("0) ATTESA A PRIORI — dichiarata nel docstring PRIMA di misurare") + print(" A1 la cella al buio NON sara' quella congelata (il criterio di scelta di k=0.30 e'") + print(" dichiarato sull'HOLD-OUT nel docstring della scoperta)") + print(" A2 il DSR si RIBALTA col conteggio: PASS fragile di famiglia, FAIL di screen") + print(" A3 boundary ROBUST ma con banda larga -> mezzanotte UTC e' un'ancora fortunata") + print(" A4 il +0,192/+0,363 di §50 e' una SOVRASTIMA (il candidato non era de-luckato)") + print(" A5 drag di funding netto positivo ma < TP01 per unita' di esposizione (0-2%/anno)") + print(" A6 frac_random_beat_hold alto (0,8-0,95) -> 'migliora l'hold-out' e' generico") + print(" A7 la finestra forward non ha potenza (t < 1) -> il gate non puo' poggiarci sopra") + + sez("1) LA FAMIGLIA — dichiarata e contata AL RIALZO, PRIMA di qualunque risultato") + print(" Regola del 30/07: riaprire un parametro riapre la sua FAMIGLIA, e i trial si") + print(" contano al RIALZO. Gradi di liberta' del segnale PREVDAY:\n") + print(f" {'grado di liberta''':<38}{'valori':>44}{'n':>6}") + righe = [("ANCHOR_DAYS (giorni del range)", str(G_ANCHOR), len(G_ANCHOR)), + ("BUFFER_K (rottura decisiva)", str(G_K), len(G_K)), + ("ALLOW_SHORT (simmetrico / long-flat)", str(G_SHORT), len(G_SHORT)), + ("MIN_HOLD_H (soppressione flip)", str(G_MH), len(G_MH)), + ("TF (barra del segnale)", str(G_TF), len(G_TF))] + for nm, vv, n in righe: + print(f" {nm:<38}{vv:>44}{n:>6}") + n_fam = len(grid) * len(tfs) + print(f" {'':<38}{'PRODOTTO = FAMIGLIA DICHIARATA':>44}{n_fam:>6}") + + sub("trial GIA' SPESI nella scoperta (giugno 2026) — sottoinsieme di questa griglia") + print(" (a) agent_09_prevday_range_breakout, 2026-06-21: il docstring dichiara il plateau") + print(" «k in 0.20..0.30» e che «below it BTC whipsaws» (quindi k piu' bassi provati) e") + print(" che «the long-flat sibling (_ALLOW_SHORT=False) is ALSO A PASS» (quindi anche") + print(" quel ramo e' stato valutato). Stima conservativa: >= 8 celle, anchor=1 sola.") + print(" (b) prevday_turnover.py, giugno: 11 celle, TUTTE con l'uplift HOLD-OUT a video —") + print(" BASE + k{0.50,0.75,1.00} + anchor{2,3,5} + min_hold{24,72} + combo-LT + long-only.") + print(" (c) prevday_bootstrap.py: 3 letture (full/short/long) della STESSA cella — non sono") + print(" trial di parametro, sono decomposizioni: contate come letture, non come celle.") + print(f" -> (a)+(b) sono tutte DENTRO la griglia di oggi: il conteggio al rialzo resta {n_fam}.") + + sub("lo SCREEN — quanti stream sono stati guardati perche' PREVDAY emergesse") + lb = json.loads(LEADERBOARD.read_text()) + print(f" (d) onda intraday 2026-06-21: {len(lb)} agenti scorati sullo stesso harness, di cui") + print(" PREVDAY e' il sopravvissuto -> e' un MASSIMO DI SCREEN, non una misura isolata.") + print(" (e) sweep 'strategie alternative' 2026-06-20: 104 ipotesi / 153 agenti sullo stesso") + print(" universo BTC/ETH (CLAUDE.md). PREVDAY non ne fa parte ma condivide harness e dato.") + n_screen = len(lb) + print(f"\n CONTEGGI CHE USERO' (tutti pubblicati, nessuno nascosto):") + print(f" N_giugno = 11 (la sola sotto-griglia gia' spesa)") + print(f" N_fam1h = {len(grid):>4} (la famiglia a TF singolo)") + print(f" N_fam = {n_fam:>4} (LA GRIGLIA DICHIARATA OGGI)") + print(f" N_+15m = {n_fam + len(grid):>4} (se si aggiungesse il TF 15m: alza N, quindi") + print(f" escluderlo rende il conto PIU' PERMISSIVO — dichiarato)") + print(f" N_screen = {n_fam + n_screen:>4} (famiglia + i {n_screen} agenti dell'onda)") + print(f" N_sweep = {n_fam + n_screen + 104:>4} (+ le 104 ipotesi del 20/06)") + return dict(n_fam=n_fam, n_fam1h=len(grid), n_screen=n_screen, lb=lb) + + +# ============================================================================================= +# 2) INTEGRITA' DEL MONITOR — sola lettura, replay offline +# ============================================================================================= +def _load_paper_module(): + """Importa scripts/live/paper_prevday.py in SOLA LETTURA (main() e' guardato da __main__). + Le costanti si DERIVANO da li' invece di ridichiararle: se il monitor cambia fee o + min-order, questo audit lo segue (regola di `fee_watch`, 21/08).""" + spec = importlib.util.spec_from_file_location( + "pp_readonly", ROOT / "scripts" / "live" / "paper_prevday.py") + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +def _fingerprint(paths) -> dict: + out = {} + for p in paths: + if p.exists(): + out[str(p)] = (p.stat().st_mtime_ns, hashlib.md5(p.read_bytes()).hexdigest()) + return out + + +def _replay(pp, st: dict, dfs: dict, corrupt_at: int | None = None) -> dict: + """Rifa' offline l'ARITMETICA di pp.advance() sulle barre certificate di OGGI, dalla barra + d'inizio del monitor a quella corrente. Nessuna scrittura. Ritorna {ts: net_modeled}.""" + data = {} + for a in ASSETS: + df = dfs[a] + c = df["close"].values.astype(float) + r = np.zeros(len(c)) + r[1:] = c[1:] / c[:-1] - 1.0 + data[a] = dict(ts=df["timestamp"].values.astype("int64"), r=r, + tgt=np.asarray(pp.prevday_target(df), float)) + common = sorted(set(data["BTC"]["ts"]).intersection(data["ETH"]["ts"])) + new_ts = [int(t) for t in common if st["start_ts"] < t <= st["last_ts"]] + idx = {a: {int(t): i for i, t in enumerate(data[a]["ts"])} for a in ASSETS} + # posizione iniziale: come init_state, ma sulle barre di OGGI (allora l'ultima era parziale) + pm = {a: float(data[a]["tgt"][idx[a][int(st["start_ts"])]]) for a in ASSETS} + out = {} + for j, t in enumerate(new_ts): + net_m = 0.0 + nm = {} + for a in ASSETS: + i = idx[a][t] + r = float(data[a]["r"][i]) + tgt = float(data[a]["tgt"][i]) + if corrupt_at is not None and j == corrupt_at and a == "BTC": + r += 0.01 # controllo positivo + net_m += pp.WEIGHT * (pm[a] * r - pp.FEE_SIDE * abs(tgt - pm[a])) + nm[a] = tgt + pm = nm + out[t] = net_m + return out + + +def sezione2(args) -> dict: + sez("2) INTEGRITA' DEL MONITOR — `paper_prevday` e' davvero l'unico monitor sano?") + print(" L'audit §32 ha trovato che 4 monitor su 6 registrano una FRAZIONE del giorno") + print(" (`advance()` consuma la barra in corso e ci porta sopra `last_ts`). `paper_prevday`") + print(" risultava l'unico sano. Qui lo si VERIFICA invece di crederlo, in SOLA LETTURA.") + watched = [PAPER_DIR / "state.json", PAPER_DIR / "returns.jsonl", + PAPER_DIR / "trades.jsonl", ROOT / "src" / "strategies" / "prevday_breakout.py", + ROOT / "scripts" / "live" / "paper_prevday.py", ROOT / "config" / "live.json"] + fp0 = _fingerprint(watched) + + pp = _load_paper_module() + print(f" costanti DERIVATE dal monitor: WEIGHT={pp.WEIGHT} FEE_SIDE={pp.FEE_SIDE} " + f"MIN_ORDER=${pp.MIN_ORDER:.0f} cap MODELED=${pp.MODELED_CAPITAL:.0f} / " + f"REAL=${pp.REAL_CAPITAL:.0f}") + st = json.loads((PAPER_DIR / "state.json").read_text()) + rec = [json.loads(l) for l in (PAPER_DIR / "returns.jsonl").read_text().splitlines() if l.strip()] + ts = np.array([r["ts"] for r in rec], dtype=np.int64) + + sub("2a) copertura temporale: barre registrate contro ore trascorse") + span_h = (st["last_ts"] - st["start_ts"]) / 3_600_000.0 + dup = len(ts) - len(np.unique(ts)) + dts = np.diff(np.sort(ts)) / 3_600_000.0 + print(f" inizio {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC')} " + f"ultima {pd.Timestamp(st['last_ts'], unit='ms', tz='UTC')}") + print(f" ore trascorse {span_h:,.0f} barre registrate {len(rec):,} duplicate {dup} " + f"passi != 1h {int((np.abs(dts - 1.0) > 1e-9).sum())}") + print(f" -> copertura NOMINALE {len(rec)/span_h:.4%} (statarb, per confronto §32: 4 min/g)") + + sub("2b) replay bit-a-bit: ogni barra registrata e' un'ORA VERA di mercato?") + dfs = {a: al.get(a, "1h") for a in ASSETS} + rep = _replay(pp, st, dfs) + common = [r for r in rec if r["ts"] in rep] + d = np.array([abs(r["net_modeled"] - round(rep[r["ts"]], 6)) for r in common]) + tol = 5e-7 # il monitor arrotonda a 6 decimali + ok = int((d <= tol).sum()) + print(f" barre confrontabili {len(common):,}/{len(rec):,} coincidenti (|diff|<={tol}) " + f"{ok:,} = {ok/max(len(common),1):.1%}") + n_days = max(span_h / 24.0, 1.0) + if len(common) - ok: + bad_ts = [r["ts"] for r, dd in zip(common, d) if dd > tol] + hh = pd.DatetimeIndex(pd.to_datetime(bad_ts, unit="ms", utc=True)) + vc = pd.Series(hh.hour).value_counts().sort_index() + print(f" divergenti {len(bad_ts):,} = {len(bad_ts)/len(common):.1%} su " + f"{n_days:.0f} giorni = {len(bad_ts)/n_days:.2f}/giorno") + print(" distribuzione per ora UTC: " + + " ".join(f"{int(h)}:00 x{int(c)}" for h, c in vc.items())) + print(" -> firma attesa e TROVATA: UNA barra al giorno, quella che il cron delle 00:30") + print(" consuma mentre e' IN CORSO. Il monitor registra ~30 min di quell'ora come se") + print(" fosse un'ora intera; i restanti ~30 min non entrano MAI (last_ts e' gia'") + print(" oltre). Le divergenze fuori dall'ora 0 sono giri partiti a un'ora diversa") + print(" (riavvii VPS, corse a mano) — stesso meccanismo, altro orario.") + print(f" ⚠️ ASSUNZIONE DICHIARATA: ~30 min persi per barra divergente (cron 00:30 +") + print(" rebuild). Non e' misurabile a posteriori — il feed di allora non esiste piu'.") + lost = len(bad_ts) / n_days * 30.0 + print(f" -> stima: {lost:.0f} min/giorno NON registrati = {1440-lost:.0f}/1440 " + f"({(1440-lost)/1440:.1%}) — contro i 4 min/giorno di `paper_statarb` (§32).") + frac_ok = ok / max(len(common), 1) + + sub("2c) controllo positivo obbligatorio — il confronto HA potenza?") + rep_bad = _replay(pp, st, dfs, corrupt_at=len(rep) // 2) + d2 = np.array([abs(r["net_modeled"] - round(rep_bad[r["ts"]], 6)) for r in common]) + n_bad = int((d2 > tol).sum()) + print(f" iniettando +1% di rendimento su UNA barra BTC: divergenti {n_bad:,} contro " + f"{len(common)-ok:,} del replay pulito -> il confronto {'SEGNALA' if n_bad > len(common)-ok else 'NON SEGNALA'}") + print(" (senza questo, 'coincide' e 'non sto confrontando niente' hanno lo stesso output)") + + sub("2d) `implausible_sharpe` sulla serie FORWARD — mai puntato sulle forward prima di §32") + fwd = pd.Series([r["net_modeled"] for r in rec], + index=pd.DatetimeIndex(pd.to_datetime([r["ts"] for r in rec], unit="ms", utc=True))) + dfwd = to_daily(fwd) + imp = al.implausible_sharpe(dfwd) + print(f" {imp}") + + fp1 = _fingerprint(watched) + assert fp0 == fp1, "VIOLAZIONE: un file sorvegliato e' cambiato durante l'audit" + print(f"\n {clock()} ✅ sola-lettura PROVATA: md5+mtime invariati su {len(fp0)} file sorvegliati") + return dict(st=st, rec=rec, fwd=fwd, dfwd=dfwd, frac_ok=frac_ok, span_h=span_h) + + +# ============================================================================================= +# 3) MDE — quanto e' distinguibile da zero un Sharpe forward su 63 giorni? +# ============================================================================================= +def _se_sharpe(sh: float, years: float) -> float: + """SE asintotico dello Sharpe annualizzato (Lo 2002, correzione per il 4o momento omessa): + SE ~ sqrt((1 + Sh^2/2)/T_anni). A Sh piccoli degenera nel sqrt(1/T) usato dal progetto.""" + return math.sqrt((1.0 + sh * sh / 2.0) / years) if years > 0 else float("nan") + + +def sezione3(M: dict) -> dict: + sez("3) MDE — la finestra forward ha potenza? (63 giorni a Sharpe +2,04)") + rec = M["rec"] + bpy = 24 * 365.25 + res = {} + print(" Due convenzioni di SE, entrambe pubblicate: `sqrt(1/T)` e' quella che il progetto") + print(" usa nelle sue note (DVOLSPREAD, BIN-FREQ); Lo (2002) `sqrt((1+Sh^2/2)/T)` e' la") + print(" corretta e a Sharpe alti e' PIU' SEVERA. La conclusione non dipende da quale si usa.") + print(f"\n {'libro':<12}{'barre':>8}{'anni':>8}{'Sharpe ann.':>13}{'SE naive':>10}{'t':>6}" + f"{'SE Lo':>8}{'t':>6}{'p (Lo)':>9}{'IC95 (Lo)':>20}") + for key, lab in (("net_modeled", "MODELED"), ("net_real", "REAL-$600")): + x = np.array([r[key] for r in rec], float) + yrs = len(x) / bpy + sh = float(x.mean() / x.std() * math.sqrt(bpy)) if x.std() > 0 else 0.0 + se_n = math.sqrt(1.0 / yrs) + se = _se_sharpe(sh, yrs) + t = sh / se + p = 2 * (1 - norm.cdf(abs(t))) + res[lab] = dict(sh=sh, se=se, se_naive=se_n, t=t, t_naive=sh / se_n, p=p, yrs=yrs) + print(f" {lab:<12}{len(x):>8,}{yrs:>8.3f}{sh:>13.2f}{se_n:>10.2f}{sh/se_n:>6.2f}" + f"{se:>8.2f}{t:>6.2f}{p:>9.3f} [{sh-1.96*se:+.2f}, {sh+1.96*se:+.2f}]") + print(f"\n 📌 L'IC95 dello Sharpe forward e' largo ~{2*1.96*res['MODELED']['se']:.1f} punti di") + print(" Sharpe. NON e' un numero che possa confermare o smentire un edge, con nessuna") + print(" delle due convenzioni (t = " + f"{res['MODELED']['t_naive']:.2f} naive / {res['MODELED']['t']:.2f} Lo, entrambi < 1).") + + sub("3b) ⚠️ e il '+2,04' pubblicato e' su una LENTE ORARIA, non su quella del progetto") + dsh = al._sh(M["dfwd"]) + print(f" Sharpe forward annualizzato dalle barre ORARIE (x sqrt(24*365,25)): " + f"{res['MODELED']['sh']:+.2f} <- il numero citato da §50") + print(f" Sharpe forward della stessa serie COMPOSTA A GIORNALIERA (x sqrt(365,25)): " + f"{dsh:+.2f}") + print(" Ogni altro numero di sleeve del progetto (TP01 1,29 · SKH01 1,45 · il libro 1,81)") + print(" e' sulla lente GIORNALIERA. Confrontare +2,04 con quelli mescola due lenti: il") + print(" numero comparabile e' " f"{dsh:+.2f}. (La differenza e' autocorrelazione oraria:") + print(" una posizione tenuta 24/7 non fa 24 scommesse indipendenti al giorno.)") + res["daily_sharpe"] = dsh + + sub("3a) quanto ci vuole? MDE (80% potenza, 5% bilaterale) per orizzonte") + print(f" {'anni':>7}{'giorni':>9}{'SE(Sh)':>9}{'MDE 80%':>10}" + f"{'P(Sh_oss>0 | vero 1.2)':>25}{'P(Sh_oss>0 | vero 0.6)':>25}") + for yrs in (res["MODELED"]["yrs"], 0.25, 0.5, 1.0, 2.0, 3.0, 5.0): + se0 = _se_sharpe(0.0, yrs) + mde = 2.802 * se0 + pw12 = norm.cdf(1.2 / _se_sharpe(1.2, yrs)) + pw06 = norm.cdf(0.6 / _se_sharpe(0.6, yrs)) + print(f" {yrs:>7.2f}{yrs*365.25:>9.0f}{se0:>9.2f}{mde:>10.2f}{pw12:>24.1%}{pw06:>24.1%}") + need12 = 2.802 ** 2 * (1 + 1.2 ** 2 / 2) / 1.2 ** 2 + need06 = 2.802 ** 2 * (1 + 0.6 ** 2 / 2) / 0.6 ** 2 + print(f"\n -> per DISTINGUERE uno Sharpe vero di 1,2 dal nulla all'80% di potenza servono " + f"{need12:.1f} anni; per 0,6 ne servono {need06:.1f}.") + print(" -> CONSEGUENZA PER IL GATE: nessuna soglia sullo Sharpe forward puo' essere") + print(" l'evidenza principale. Una soglia ALTA sarebbe finta precisione (stessa lezione") + print(" scritta per DVOLSPREAD); una soglia BASSA vale solo come KILL, perche' li' la") + print(" bassa potenza lavora nel verso giusto: si uccide solo cio' che e' molto negativo.") + print(f" -> P(un edge VIVO a Sharpe 1,2 venga ucciso da una soglia a {KILL_SHARPE:+.2f} " + f"a 1 anno) = {norm.cdf((KILL_SHARPE-1.2)/_se_sharpe(1.2,1.0)):.2%}") + return res + + +# ============================================================================================= +# 4) `study_family_honest` — selezione in-sample-only -> DSR -> marginal_vs_tp01 +# ============================================================================================= +def sezione4(grid: list, tfs: tuple) -> dict: + sez("4) LA FAMIGLIA ONESTA — la cella scelta AL BUIO e' quella che gira nel monitor?") + print(" Si usano i TRE componenti REALI di `study_family_honest` (importati, non riscritti):") + print(" `select_cell_insample` -> `deflated_sharpe` -> `study_marginal`. Sono chiamati in") + print(" sequenza invece che tramite il wrapper solo per non ricalcolare la griglia due") + print(" volte; l'IDENTITA' della composizione col wrapper e' verificata sotto (4d).") + + sub("4a) controllo di replica: la cella congelata E' `prevday_breakout.target`?") + for a in ASSETS: + df = al.get(a, "1h") + d1 = np.nan_to_num(np.asarray(pb.target(df), float)) + d2 = np.nan_to_num(np.asarray(frozen_fn()(df), float)) + md = float(np.max(np.abs(d1 - d2))) + print(f" {a}: max|factory(FROZEN) - pb.target| = {md:.1e} " + f"{'✅ bit-exact' if md == 0.0 else '❌ DIVERGE'}") + assert md == 0.0, "la factory non riproduce il segnale congelato" + cz = al.causality_ok(frozen_fn(), tf="1h") + print(f" causality_ok: {cz}") + + sub(f"4b) selezione IN-SAMPLE-ONLY su {len(grid)*len(tfs)} celle (pre-{HOLDOUT.date()})") + t = time.time() + sel = al.select_cell_insample(factory, grid, tfs) + ch = sel["chosen"] + print(f" {clock()} {len(sel['all_full_sharpe'])} celle valutate in {time.time()-t:.0f}s") + rows = sel["rows"] + fro = [r for r in rows if r["tf"] == FROZEN_TF and r["params"] == FROZEN] + rank_fro = rows.index(fro[0]) + 1 if fro else None + print(f"\n cella scelta AL BUIO : tf={ch['tf']} {ch['params']} " + f"Sh in-sample {ch['insample_sharpe']:+.3f} Sh FULL {ch['full_sharpe']:+.3f}") + if fro: + print(f" cella CONGELATA : tf={FROZEN_TF} {FROZEN} " + f"Sh in-sample {fro[0]['insample_sharpe']:+.3f} Sh FULL {fro[0]['full_sharpe']:+.3f}") + print(f" rango della congelata in-sample: {rank_fro}/{len(rows)}") + uguale = bool(fro and ch["tf"] == FROZEN_TF and ch["params"] == FROZEN) + print(f"\n ➜ LA CELLA AL BUIO {'COINCIDE' if uguale else 'NON COINCIDE'} CON QUELLA CHE GIRA.") + + print(f"\n {'top-10 IN-SAMPLE':<34}{'tf':>4}{'Sh IS':>9}{'Sh FULL':>10}") + for r in rows[:10]: + p = r["params"] + lab = f"anchor={p['anchor']} k={p['k']:.2f} short={int(p['short'])} mh={p['mh']}" + star = " <= CONGELATA" if (r["tf"] == FROZEN_TF and p == FROZEN) else "" + print(f" {lab:<34}{r['tf']:>4}{r['insample_sharpe']:>+9.3f}{r['full_sharpe']:>+10.3f}{star}") + + sub("4c) il candidato al buio passa `marginal_vs_tp01` e il deflated-Sharpe?") + fn = factory(tf=ch["tf"], **ch["params"]) + daily = al.candidate_daily(fn, tf=ch["tf"]) + sm = al.study_marginal(f"PREVDAY ISpick {ch['params']}", fn, tf=ch["tf"]) + dsr, sr0 = al.deflated_sharpe(al._sh(daily), sel["all_full_sharpe"], daily) + print(al.fmt_marginal(sm)) + print(f"\n deflated-Sharpe (N={len(sel['all_full_sharpe'])}, famiglia): {dsr:.3f} " + f"[soglia {GATE_DSR_MIN}] massimo atteso dal solo rumore {sr0:.3f}") + print(f" earns_slot_honest = {bool(sm['earns_slot'] and dsr >= GATE_DSR_MIN)}") + + sub("4d) IDENTITA': la mia composizione == `study_family_honest`?") + tiny = [dict(anchor=1, k=0.30, short=True, mh=0), dict(anchor=1, k=0.20, short=True, mh=0)] + w = al.study_family_honest("identita", factory, tiny, ("1h",)) + s2 = al.select_cell_insample(factory, tiny, ("1h",)) + c2 = s2["chosen"] + f2 = factory(tf=c2["tf"], **c2["params"]) + d2 = al.candidate_daily(f2, tf=c2["tf"]) + dsr2, _ = al.deflated_sharpe(al._sh(d2), s2["all_full_sharpe"], d2) + same = (w["chosen"]["params"] == c2["params"] and + abs((w["deflated_sharpe"] or 0) - round(dsr2, 3)) < 1e-9) + print(f" wrapper: cella {w['chosen']['params']} DSR {w['deflated_sharpe']} | " + f"composizione: {c2['params']} DSR {round(dsr2,3)} -> {'✅ IDENTICI' if same else '❌ DIVERGONO'}") + + # la cella CONGELATA, valutata con gli stessi strumenti (e' quella che gira nel monitor) + sub("4e) e la cella CONGELATA (quella che gira), giudicata con gli stessi strumenti") + dfroz = al.candidate_daily(frozen_fn(), tf=FROZEN_TF) + smf = al.study_marginal("PREVDAY CONGELATA", frozen_fn(), tf=FROZEN_TF) + dsrf, sr0f = al.deflated_sharpe(al._sh(dfroz), sel["all_full_sharpe"], dfroz) + print(f" Sh FULL {al._sh(dfroz):+.3f} Sh IS {al._sh(dfroz[dfroz.index=HOLDOUT]):+.3f} maxDD {100*al._dd_ret(dfroz):.1f}%") + print(f" abs={smf['abs_grade']} marginal={smf['marginal_verdict']} " + f"earns_slot={smf['earns_slot']} robust_oos={smf['marginal'].get('robust_oos')} " + f"is_hedge={smf['marginal'].get('is_hedge')}") + print(f" deflated-Sharpe (N={len(sel['all_full_sharpe'])}): {dsrf:.3f}") + print(f" ⚠️ la congelata NON e' selezionabile in-sample (rango {rank_fro}/{len(rows)}): il suo") + print(" DSR e' riportato per completezza, non come verdetto — il verdetto e' 4c.") + + sub("4f) LE DUE CELLE A CONFRONTO — non sono la stessa strategia") + mb, mf = sm["marginal"], smf["marginal"] + print(f" {'':<24}{'AL BUIO (in-sample)':>22}{'CONGELATA (monitor)':>22}") + rr = [("tf / parametri", f"{ch['tf']} k={ch['params']['k']:.2f} " + f"short={int(ch['params']['short'])}", + f"{FROZEN_TF} k={FROZEN['k']:.2f} short={int(FROZEN['short'])}"), + ("rango in-sample", f"1/{len(rows)}", f"{rank_fro}/{len(rows)}"), + ("Sharpe FULL", f"{al._sh(daily):+.3f}", f"{al._sh(dfroz):+.3f}"), + ("Sharpe HOLD-OUT", f"{al._sh(daily[daily.index>=HOLDOUT]):+.3f}", + f"{al._sh(dfroz[dfroz.index>=HOLDOUT]):+.3f}"), + ("maxDD", f"{100*al._dd_ret(daily):.1f}%", f"{100*al._dd_ret(dfroz):.1f}%"), + ("corr -> TP01 (full)", f"{mb.get('corr_full')}", f"{mf.get('corr_full')}"), + ("corr -> TP01 (hold)", f"{mb.get('corr_hold')}", f"{mf.get('corr_hold')}"), + ("beta a TP01", f"{mb.get('beta_to_tp01')}", f"{mf.get('beta_to_tp01')}"), + ("uplift w25 full/hold", + f"{mb['blends']['w25']['uplift_full']:+.3f}/{mb['blends']['w25']['uplift_hold']:+.3f}", + f"{mf['blends']['w25']['uplift_full']:+.3f}/{mf['blends']['w25']['uplift_hold']:+.3f}"), + ("deflated-Sharpe N=336", f"{dsr:.3f}", f"{dsrf:.3f}")] + for nm, a_, b_ in rr: + print(f" {nm:<24}{a_:>22}{b_:>22}") + print() + print(" 🚨 IL PUNTO, ed e' il risultato principale di questa sezione: la selezione onesta") + print(f" sceglie il libro LONG-FLAT (short={int(ch['params']['short'])}), mentre il monitor") + print(" gira il libro SIMMETRICO. Il docstring della scoperta dice l'OPPOSTO in chiaro:") + print(" «LONG-SHORT is the slot-earner ... the long-flat sibling correlates ~0.64 to TP01") + print(" (it just re-rides the bull) -> a much smaller marginal uplift».") + print(" La gamba SHORT — cioe' l'intera ragione per cui PREVDAY fu promosso a LEAD — e'") + print(" esattamente cio' che la selezione in-sample-only NON compra.") + print(" ⚠️ E il rovescio, che va detto: la cella al buio sta a corr " + f"{mb.get('corr_full')} da TP01 contro {mf.get('corr_full')} della congelata. Passa") + print(" `marginal_vs_tp01` (ADDS, alpha residua " + f"{mb.get('alpha_ann'):+.1%}/anno), ma e' molto piu' vicina a TP01: il candidato che") + print(" sopravvive alla selezione onesta e' anche quello che diversifica di meno.") + + sub("4g) `implausible_sharpe` su entrambe (retroattivo, come §26 su tutti gli sleeve)") + for nm, dd_ in (("AL BUIO", daily), ("CONGELATA", dfroz)): + i = al.implausible_sharpe(dd_) + print(f" {nm:<12}implausible={i['implausible']} Sharpe {i['sharpe']:.2f} " + f"maxDD {100*i['maxdd']:.1f}% perdite/attive {i['loss_frac']:.1%} " + f"Calmar {i['calmar']:.1f} reasons={i['reasons']}") + return dict(sel=sel, chosen=ch, daily=daily, sm=sm, dsr=dsr, sr0=sr0, + daily_frozen=dfroz, sm_frozen=smf, dsr_frozen=dsrf, + uguale=uguale, rank_frozen=rank_fro) + + +# ============================================================================================= +# 5) DSR — sensibilita' del verdetto al CONTEGGIO e alla PARTIZIONE +# ============================================================================================= +def _synth_trials(n: int, sd: float, center: float) -> np.ndarray: + """Array sintetico di n Sharpe con deviazione standard CAMPIONARIA esattamente `sd`. + Serve a riparametrizzare `al.deflated_sharpe` per (N, sd) usando la SUA formula, non una + mia copia. L'identita' col caso reale e' verificata in 5a.""" + z = np.linspace(-1.0, 1.0, n) + z = (z - z.mean()) / z.std(ddof=1) + return center + sd * z + + +def sezione5(S4: dict, F: dict) -> dict: + sez("5) DEFLATED-SHARPE — il verdetto dipende dal CONTEGGIO e dalla PARTIZIONE") + print(" Il 22/08 tre agenti indipendenti hanno misurato che `altlib.deflated_sharpe` non") + print(" dipende solo da N ma dalla VARIANZA della griglia (sr0 = sd(trial) x mult(N)): su") + print(" una famiglia omogenea passa quasi sempre, e la STESSA griglia da' verdetti opposti") + print(" secondo come la si partiziona. Qui la sensibilita' e' PUBBLICATA, non nascosta.") + sel, daily = S4["sel"], S4["daily"] + allsr = np.array([s for s in sel["all_full_sharpe"] if np.isfinite(s)], float) + sh = al._sh(daily) + rows = sel["rows"] + + sub("5a) controllo: la riparametrizzazione (N, sd) riproduce il caso reale?") + d_real, sr0_real = al.deflated_sharpe(sh, list(allsr), daily) + d_syn, sr0_syn = al.deflated_sharpe(sh, list(_synth_trials(len(allsr), allsr.std(ddof=1), + allsr.mean())), daily) + print(f" reale N={len(allsr)} sd={allsr.std(ddof=1):.3f} -> DSR {d_real:.4f} (sr0 {sr0_real:.3f})") + print(f" sint. N={len(allsr)} sd={allsr.std(ddof=1):.3f} -> DSR {d_syn:.4f} (sr0 {sr0_syn:.3f})") + print(f" scarto {abs(d_real-d_syn):.1e} {'✅' if abs(d_real-d_syn) < 1e-9 else '⚠️ non identici'}") + + sub("5b) PARTIZIONI della STESSA griglia — ognuna e' difendibile, e danno verdetti diversi") + def part(nome, pred): + sub_rows = [r for r in rows if pred(r)] + srs = [r["full_sharpe"] for r in sub_rows] + if len(srs) < 2: + return None + d, s0 = al.deflated_sharpe(sh, srs, daily) + return dict(nome=nome, n=len(srs), sd=float(np.std(srs, ddof=1)), dsr=d, sr0=s0) + giugno = [dict(anchor=1, k=0.30, short=True, mh=0), dict(anchor=1, k=0.50, short=True, mh=0), + dict(anchor=1, k=0.75, short=True, mh=0), dict(anchor=1, k=1.00, short=True, mh=0), + dict(anchor=2, k=0.30, short=True, mh=0), dict(anchor=3, k=0.30, short=True, mh=0), + dict(anchor=5, k=0.30, short=True, mh=0), dict(anchor=1, k=0.30, short=True, mh=24), + dict(anchor=1, k=0.30, short=True, mh=72), dict(anchor=2, k=0.75, short=True, mh=24), + dict(anchor=1, k=0.30, short=False, mh=0)] + parts = [ + part("N_giugno (le 11 celle gia' spese)", + lambda r: r["tf"] == "1h" and r["params"] in giugno), + part("solo l'asse k (anchor=1, mh=0, short)", + lambda r: r["tf"] == "1h" and r["params"]["anchor"] == 1 + and r["params"]["mh"] == 0 and r["params"]["short"]), + part("libro simmetrico, senza min-hold, 1h", + lambda r: r["tf"] == "1h" and r["params"]["mh"] == 0 and r["params"]["short"]), + part("tutto 1h", lambda r: r["tf"] == "1h"), + part("LA FAMIGLIA DICHIARATA (1h + 4h)", lambda r: True), + ] + print(f" {'partizione':<40}{'N':>5}{'sd(Sh)':>9}{'sr0':>8}{'DSR':>9}{'verdetto':>10}") + for p in parts: + if p is None: + continue + print(f" {p['nome']:<40}{p['n']:>5}{p['sd']:>9.3f}{p['sr0']:>8.3f}{p['dsr']:>9.3f}" + f"{('PASS' if p['dsr'] >= GATE_DSR_MIN else 'FAIL'):>10}") + + sub("5c) sr0 di SCREEN — non basta contare i trial, serve la VARIANZA di screen") + lb = F["lb"] + scr = np.array([x["abs_full_sharpe"] for x in lb], float) + print(f" i {len(scr)} agenti dell'onda intraday 2026-06-21: Sharpe da {scr.min():.2f} a " + f"{scr.max():.2f}, media {scr.mean():.2f}, sd {scr.std(ddof=1):.3f}") + print(" ⚠️ lente DICHIARATA: `abs_full_sharpe` del leaderboard e' la Sharpe per-asset del") + print(" giudice di giugno, non la 50/50 giornaliera di `candidate_daily`. Serve per la") + print(" sua DISPERSIONE, non per il livello.") + mix = np.concatenate([allsr, scr]) + d_scr, s0_scr = al.deflated_sharpe(sh, list(mix), daily) + print(f" famiglia+screen: N={len(mix)} sd={mix.std(ddof=1):.3f} sr0={s0_scr:.3f} " + f"DSR={d_scr:.3f} {'PASS' if d_scr >= GATE_DSR_MIN else 'FAIL'}") + + sub("5d) la superficie DSR(N, sd) — dove sta il verdetto, e quanto e' fragile") + sds = (0.15, 0.20, 0.25, 0.30, 0.40, 0.58) + ns = (11, 56, 168, 336, 504, 456, 560) + print(" " + "N su sd".rjust(8) + "".join(f"{s:>9.2f}" for s in sds)) + for n in sorted(ns): + cells = [] + for s in sds: + dd, _ = al.deflated_sharpe(sh, list(_synth_trials(n, s, float(allsr.mean()))), daily) + cells.append(f"{dd:>9.3f}") + print(f" {n:>8}" + "".join(cells)) + print(" (sd=0,58 e' il valore implicito dell'ORTHO-SCREEN del 22/08; la riga N=456 e' la") + print(f" famiglia+screen di 5c. Soglia {GATE_DSR_MIN}.)") + + sub("5e) ➜ IL VERDETTO NON SI RIBALTA COL CONTEGGIO. Si ribalta con la VARIANZA.") + lo_, hi_ = 0.05, 3.0 + for _ in range(60): + mid = 0.5 * (lo_ + hi_) + dd, _s = al.deflated_sharpe(sh, list(_synth_trials(len(allsr), mid, + float(allsr.mean()))), daily) + if dd >= GATE_DSR_MIN: + lo_ = mid + else: + hi_ = mid + print(f" A N={len(allsr)} il DSR scende sotto {GATE_DSR_MIN} solo per sd(trial) >= " + f"{0.5*(lo_+hi_):.3f}.") + print(f" La famiglia ne ha {allsr.std(ddof=1):.3f}: servirebbe una dispersione " + f"{0.5*(lo_+hi_)/allsr.std(ddof=1):.1f}x per bocciarla.") + print(" Portare N da 11 a 560 costa " + + f"{al.deflated_sharpe(sh, list(_synth_trials(11, allsr.std(ddof=1), float(allsr.mean()))), daily)[0] - al.deflated_sharpe(sh, list(_synth_trials(560, allsr.std(ddof=1), float(allsr.mean()))), daily)[0]:.3f}" + " di DSR: il conteggio, su questa famiglia, NON e' la leva.") + print(" ⚠️ ATTESA A PRIORI A2 REFUTATA. Avevo previsto che il verdetto si ribaltasse col") + print(" conteggio (come VRP01 il 30/07: 0,983 a N=8 / 0,948 a N=72). Qui NON succede, e") + print(" il motivo e' MISURABILE: la famiglia PREVDAY e' OMOGENEA (le sue celle si") + print(" assomigliano, sd 0,27) mentre la griglia VRP01 e la griglia ORTHO erano") + print(" eterogenee (sd ~0,58). `sr0 = sd(trial) x mult(N)` e mult(N) cresce come una") + print(" radice di log: da N=11 a N=560 vale ~1,8x, mentre sd puo' variare 4x fra") + print(" famiglie. ➜ REGOLA: il deflated-Sharpe di una famiglia OMOGENEA e' quasi") + print(" INSENSIBILE al numero di trial — dichiarare 11 celle o 560 cambia 0,009. Chi") + print(" discute il CONTEGGIO su una famiglia stretta sta discutendo la variabile") + print(" sbagliata; l'unica leva e' la DISPERSIONE, che si sceglie decidendo dove") + print(" mettere il bordo della famiglia. ⚠️ Cio' NON vuol dire che il gate sia vacuo:") + print(" 5f mostra che ha potenza sul LIVELLO (la cella mediana della stessa famiglia") + print(" FALLISCE). E' cieco su un asse e vede sull'altro — e il 30/07 il verdetto su") + print(" VRP01 si ribaltava proprio perche' quella famiglia era larga.") + + sub("5f) controllo positivo del gate — passerebbe anche una cella MEDIOCRE?") + med = float(np.median(allsr)) + d_med, _ = al.deflated_sharpe(med, list(allsr), daily) + d_lo, _ = al.deflated_sharpe(float(np.percentile(allsr, 25)), list(allsr), daily) + print(f" candidato con Sharpe = MEDIANA della famiglia ({med:+.3f}): DSR {d_med:.3f} " + f"-> {'PASS' if d_med >= GATE_DSR_MIN else 'FAIL'}") + print(f" candidato con Sharpe = 25o pctl della famiglia ({np.percentile(allsr,25):+.3f}): " + f"DSR {d_lo:.3f} -> {'PASS' if d_lo >= GATE_DSR_MIN else 'FAIL'}") + print(" (approssimazione DICHIARATA: si tiene la serie del vincitore per skew/kurt/T e si") + print(" sostituisce solo lo Sharpe. Se anche la mediana passa, il gate su questa famiglia") + print(" non sta selezionando: sta contando.)") + return dict(parts=[p for p in parts if p], dsr_screen=d_scr, allsr=allsr, sh=sh, + sd_soglia=0.5 * (lo_ + hi_), dsr_mediana=d_med) + + +# ============================================================================================= +# 6) `day_boundary_robust` — il segnale VIVE sul confine del giorno UTC +# ============================================================================================= +def _hour_of_day_signal(df: pd.DataFrame) -> np.ndarray: + """CONTROLLO POSITIVO: segnale PURAMENTE di calendario (long la prima meta' del giorno UTC, + short la seconda). Deve INVERTIRSI spostando il confine di 12h. Se non si inverte, il test + non ha potenza e il verdetto su PREVDAY non vale niente.""" + h = pd.to_datetime(df["datetime"], utc=True).dt.hour.values + d = np.where(h < 12, 1.0, -1.0) + return np.nan_to_num(pb._vol_target(d, df, pb.TARGET_VOL, pb.VOL_WIN_DAYS, pb.LEV_CAP), nan=0.0) + + +def sezione6() -> dict: + sez("6) `day_boundary_robust` — PREVDAY e' un segnale di prezzo o di ETICHETTA?") + print(" Il livello di PREVDAY E' fissato dal rollover di mezzanotte UTC: e' esattamente la") + print(" classe di segnali per cui questo gate e' nato (open_drive, 2026-06-21). Il") + print(" leaderboard di giugno lo dichiara ROBUST con spread 0,196 — cioe' NON invariante.") + print(" Qui si ri-misura invece di fidarsi, agli offset richiesti +-2 / +-4 / +-8 ore.") + + sub("6a) controllo positivo — il test HA potenza?") + ctl = al.day_boundary_robust(_hour_of_day_signal, tf="1h", offsets=(0, 6, 12, 18)) + print(f" segnale puramente di calendario (long h<12): {ctl['per_offset']} " + f"spread {ctl['spread']} verdetto {ctl['verdict']}") + ok_ctl = ctl["verdict"] == "ARTIFACT-RISK" + print(f" {'✅ il gate lo SMASCHERA' if ok_ctl else '⚠️ il gate NON lo smaschera'} " + f"(atteso ARTIFACT-RISK: a +12h il segnale e' il proprio opposto)") + + sub("6b) PREVDAY congelato, offset richiesti") + r = al.day_boundary_robust(frozen_fn(), tf="1h", offsets=(-8, -4, -2, 0, 2, 4, 8)) + for o in sorted(r["per_offset"]): + print(f" {o:>+4}h uplift w25 {r['per_offset'][o]:+.3f}") + print(f" a 0h {r['per_offset'][0]:+.3f} min {r['min']:+.3f} max {r['max']:+.3f} " + f"spread {r['spread']:.3f} verdetto {r['verdict']}") + print(f" ⚠️ il campo `base` di altlib e' per_offset[offsets[0]] = l'offset {(-8):+d}h, NON lo") + print(" zero: con una griglia che comincia in negativo chiamarlo 'base' inganna.") + print(f" -> l'uplift {'NON cambia segno' if r['robust_to_boundary'] else 'CAMBIA SEGNO'}: " + f"{'non e' + chr(39) + ' un artefatto di etichettatura' if r['robust_to_boundary'] else 'ARTEFATTO'}") + + sub("6c) griglia oraria PIENA: 24 confini-giorno (standalone + uplift)") + B = al.tp01_baseline_daily() + per = {} + for off in PV_BOUNDARY: + series = {} + for a in ASSETS: + df0 = al.get(a, "1h") + tgt = frozen_fn()(al._shift_calendar(df0, off) if off else df0) + ev = al.eval_weights(df0, tgt) + series[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + cand = to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + JJ = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna() + per[off] = dict(full=al._sh(cand), hold=al._sh(cand[cand.index >= HOLDOUT]), + dd=al._dd_ret(cand), + upl=al._sh(0.75 * JJ["B"] + 0.25 * JJ["C"]) - al._sh(JJ["B"]), + daily=cand) + fu = np.array([per[o]["full"] for o in PV_BOUNDARY]) + ho = np.array([per[o]["hold"] for o in PV_BOUNDARY]) + up = np.array([per[o]["upl"] for o in PV_BOUNDARY]) + dd = np.array([per[o]["dd"] for o in PV_BOUNDARY]) + print(f" {'metrica':<16}{'canonico (0h)':>15}{'pctl':>7}{'MEDIANA onesta':>17}" + f"{'[p10, p90]':>20}{'fortuna':>10}") + for nm, v in (("Sharpe FULL", fu), ("Sharpe HOLD-OUT", ho), ("uplift w25", up)): + pc = float((v < v[0]).mean() * 100) + print(f" {nm:<16}{v[0]:>+15.3f}{pc:>6.0f}°{np.median(v):>+17.3f}" + f" [{np.percentile(v,10):+.3f}, {np.percentile(v,90):+.3f}]{v[0]-np.median(v):>+10.3f}") + pcd = float((dd > dd[0]).mean() * 100) + print(f" {'maxDD':<16}{100*dd[0]:>14.1f}%{pcd:>6.0f}°{100*np.median(dd):>16.1f}%" + f" [{100*np.percentile(dd,10):.1f}%, {100*np.percentile(dd,90):.1f}%]" + f"{100*(dd[0]-np.median(dd)):>+9.1f}") + print(" (pctl = quante ancore stanno SOTTO la canonica; per il maxDD, quante stanno sopra") + print(" cioe' peggio. `anchor_luck_band` di altlib fa lo stesso conto: la mediana e' la") + print(" stima onesta, la differenza canonico-mediana e' la fortuna.)") + return dict(req=r, per=per, ctl_ok=ok_ctl) + + +# ============================================================================================= +# 7) BANDA D'ANCORA DEL LIBRO — differenze APPAIATE sullo spazio congiunto +# ============================================================================================= +def prevday_daily_off(off: int, rate: dict | None) -> pd.Series: + """Serie giornaliera 50/50 di PREVDAY al confine-giorno `off`, col FUNDING dentro. + Replica ESATTA di r0823_book_3rd.prevday() a off=0 (verificata bit-exact in 7a).""" + series = {} + for a in ASSETS: + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray( + frozen_fn()(al._shift_calendar(df, off) if off else df), float)) + r = al.simple_returns(df["close"].values.astype(float)) + pos = np.zeros(len(tgt)) + pos[1:] = tgt[:-1] + net = pos * r - FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + if rate is not None: + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + net = net - pos * fh + net[0] = 0.0 + series[a] = pd.Series(net, index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)) + + +def _stats(s: pd.Series): + m = metrics(s) + mh = metrics(s[s.index >= HOLDOUT]) + return m["sharpe"], mh["sharpe"], m["cagr"], m["maxdd"] + + +def sezione7(rate: dict, draws: int) -> dict: + sez("7) BANDA D'ANCORA DEL LIBRO — §50 ha de-luckato TP01 e SKH01, NON il candidato") + print(" §50 ha estratto TP01 x24 ancore e SKH01 x23 offset e ha tenuto PREVDAY FERMO al suo") + print(" confine canonico (`ANCHOR_OF['PREVDAY'] = (0,)`). Ma PREVDAY UN'ANCORA CE L'HA — e'") + print(" l'ora del rollover che fissa il livello — e la sezione 6c la misura fortunata. Un") + print(" delta e' un delta: eredita la fortuna dell'ancora (lezione 26/07). Qui lo spazio") + print(f" congiunto e' TP01 x{len(TP_HOURS)} · SKH01 x{len(SKH_OFFSETS)} · PREVDAY x{len(PV_BOUNDARY)} " + f"= {len(TP_HOURS)*len(SKH_OFFSETS)*len(PV_BOUNDARY):,} configurazioni.") + + if not B3RD_CACHE.exists(): + print(f" ⚠️ cache §50 assente ({B3RD_CACHE}) -> sezione 7 SALTATA (non e' un risultato).") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + + sub("7a) sanity obbligatorio: la mia PREVDAY a off=0 e' quella di §50?") + p0 = prevday_daily_off(0, rate) + ref = S["PREVDAY"][0] + j = pd.concat({"a": p0, "b": ref}, axis=1, join="inner") + md = float(np.max(np.abs(j["a"] - j["b"]))) + print(f" max|mia - §50| = {md:.2e} su {len(j):,} giorni " + f"{'✅ bit-exact (e conferma che la cache ha il funding dentro)' if md < 1e-12 else '⚠️ DIVERGE'}") + + t = time.time() + PV = {o: (p0 if o == 0 else prevday_daily_off(o, rate)) for o in PV_BOUNDARY} + print(f" {clock()} PREVDAY x{len(PV_BOUNDARY)} confini-giorno costruita ({time.time()-t:.0f}s)") + + sub(f"7b) {draws} estrazioni CONGIUNTE — mediana delle DIFFERENZE APPAIATE") + rng = np.random.default_rng(SEED) + hs = rng.choice(TP_HOURS, size=draws) + os_ = rng.choice(SKH_OFFSETS, size=draws) + bs_ = rng.choice(PV_BOUNDARY, size=draws) + out = {w: {"sh": [], "shh": [], "cagr": [], "dd": []} for w in WEIGHTS_TESTED} + out_can = {w: {"sh": [], "shh": []} for w in WEIGHTS_TESTED} # candidato FERMO a off=0 + base = [] + for i in range(draws): + cols2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]} + b = combine_outer(cols2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START) + bst = _stats(b) + base.append(bst) + for w in WEIGHTS_TESTED: + wm = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "CAND": w} + a = combine_outer({**cols2, "CAND": PV[bs_[i]]}, wm, lo=BOOK_START) + st = _stats(a) + out[w]["sh"].append(st[0] - bst[0]); out[w]["shh"].append(st[1] - bst[1]) + out[w]["cagr"].append(st[2] - bst[2]); out[w]["dd"].append(st[3] - bst[3]) + c = combine_outer({**cols2, "CAND": PV[0]}, wm, lo=BOOK_START) + sc = _stats(c) + out_can[w]["sh"].append(sc[0] - bst[0]); out_can[w]["shh"].append(sc[1] - bst[1]) + B = np.array(base) + print(f" libro live de-luckato (2 sleeve): ShFULL {np.median(B[:,0]):.3f} " + f"ShHOLD {np.median(B[:,1]):.3f} CAGR {100*np.median(B[:,2]):.2f}% " + f"maxDD {100*np.median(B[:,3]):.2f}%") + print(f"\n {'peso':>6}{'dShFULL':>11}{'>0':>6}{'[p10,p90]':>19}{'dShHOLD':>11}{'>0':>6}" + f"{'d CAGR pp':>11}{'d maxDD pp':>12}{' | §50 (cand. FERMO a 0h)':>28}") + res = {} + for w in WEIGHTS_TESTED: + a = np.array(out[w]["sh"]); ah = np.array(out[w]["shh"]) + ac = np.array(out[w]["cagr"]); ad = np.array(out[w]["dd"]) + c = np.array(out_can[w]["sh"]); ch = np.array(out_can[w]["shh"]) + res[w] = dict(sh=float(np.median(a)), sh_pos=float((a > 0).mean()), + shh=float(np.median(ah)), shh_pos=float((ah > 0).mean()), + cagr=float(np.median(ac)), dd=float(np.median(ad)), + sh_can=float(np.median(c)), shh_can=float(np.median(ch))) + print(f" {w:>5.0%}{np.median(a):>+11.4f}{100*(a>0).mean():>5.0f}%" + f" [{np.percentile(a,10):+.3f},{np.percentile(a,90):+.3f}]" + f"{np.median(ah):>+11.4f}{100*(ah>0).mean():>5.0f}%" + f"{100*np.median(ac):>+11.2f}{100*np.median(ad):>+12.2f}" + f" FULL {np.median(c):+.3f} HOLD {np.median(ch):+.3f}") + w = W_PROPOSTO + print("\n ✅ REPLICA INDIPENDENTE DI §50: la colonna 'cand. FERMO a 0h' e' lo stesso conto") + print(f" che ha fatto §50 (candidato all'ancora canonica). Al 15% da' FULL " + f"{res[w]['sh_can']:+.3f} / HOLD {res[w]['shh_can']:+.3f} contro i +0,192 / +0,363") + print(" pubblicati -> macchineria e cache riprodotte, il confronto e' like-with-like.") + print(f"\n ➜ AL PESO PROPOSTO ({w:.0%}), de-luckare ANCHE IL CANDIDATO costa " + f"{res[w]['sh']-res[w]['sh_can']:+.4f} di dShFULL e " + f"{res[w]['shh']-res[w]['shh_can']:+.4f} di dShHOLD:") + print(f" dShFULL {res[w]['sh_can']:+.3f} -> {res[w]['sh']:+.3f} " + f"(-{100*(1-res[w]['sh']/res[w]['sh_can']):.0f}%) " + f"dShHOLD {res[w]['shh_can']:+.3f} -> {res[w]['shh']:+.3f} " + f"(-{100*(1-res[w]['shh']/res[w]['shh_can']):.0f}%)") + print(" 📌 Il contributo RESTA positivo al 100% delle estrazioni a ogni peso: il segno non") + print(" dipende dall'ancora, la TAGLIA si'. E' la stessa forma del 22/08 su GATE PROP-01:") + print(" «un gate superato non e' un numero confermato».") + return res + + +# ============================================================================================= +# 8) FUNDING — un long/short vol-targeted su perpetual: paga o incassa? +# ============================================================================================= +def sezione8(rate: dict) -> dict: + sez("8) FUNDING — il costo di TENERE APERTA una posizione, che nessun backtest storico ha") + print(" Il 22/08 ha misurato che il libro paga -2,16%/anno di drift e che ESPOSIZIONE e") + print(" FUNDING sono positivamente correlati (il tasso pagato davvero e' 1,9-3,4x") + print(" l'incondizionato). PREVDAY e' long-short: la gamba SHORT lo INCASSA. Quale vince?") + out = {} + print(f"\n {'asset':>6}{'%long':>8}{'%short':>8}{'|pos| medio':>12}{'incond./anno':>14}" + f"{'| cond. LONG':>14}{'rapp.':>7}{'| cond. SHORT':>15}{'rapp.':>7}{'NETTO/anno':>12}") + legs = {} + for a in ASSETS: + df = al.get(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + tgt = np.nan_to_num(np.asarray(frozen_fn()(df), float)) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] + t = FU.to_ms(idx) + fh = FU.cumF(rate[a], t + FU.MSH) - FU.cumF(rate[a], t) + yrs = len(pos) / (24 * 365.25) + ser = rate[a]["series"] + ser = ser[(ser.index >= idx.min()) & (ser.index <= idx.max())] + unc = float(ser.mean()) * 24 * 365.25 + pl = np.maximum(pos, 0.0); ps = -np.minimum(pos, 0.0) + cl = float(np.sum(pl * fh) / pl.sum()) * 24 * 365.25 if pl.sum() > 0 else float("nan") + cs = float(np.sum(ps * fh) / ps.sum()) * 24 * 365.25 if ps.sum() > 0 else float("nan") + net = float(np.sum(pos * fh)) / yrs + legs[a] = dict(pos=pos, fh=fh, idx=idx, net=net, unc=unc, cl=cl, cs=cs) + print(f" {a:>6}{float((pos>0).mean()):>8.1%}{float((pos<0).mean()):>8.1%}" + f"{float(np.abs(pos).mean()):>12.3f}{unc:>14.2%}{cl:>14.2%}{cl/unc:>6.2f}x" + f"{cs:>15.2%}{cs/unc:>6.2f}x{net:>12.3%}") + sleeve = float(np.mean([legs[a]["net"] for a in ASSETS])) + out["sleeve_drag"] = sleeve + print(f" -> drag NETTO dello sleeve (le due gambe pesano 0,5 ciascuna): {sleeve:.3%}/anno") + print(f" -> al peso di libro proposto ({W_PROPOSTO:.0%}): {W_PROPOSTO*sleeve:.3%}/anno di libro") + + print("\n 📌 MECCANISMO: se `cond. LONG` e `cond. SHORT` sono ENTRAMBI sopra l'incondizionato,") + print(" le due gambe NON si compensano — la lunga paga caro E la corta incassa caro, e il") + print(" netto dipende da quanto tempo sta da ciascun lato. Se invece il rapporto della") + print(" corta e' < 1, la corta incassa POCO e il costo resta tutto sulla lunga.") + + sub("8b) l'effetto sui numeri di PREVDAY (unico grado di liberta': il funding)") + a0 = prevday_daily_off(0, None) + a1 = prevday_daily_off(0, rate) + print(f" {'lente':<22}{'Sh FULL':>10}{'Sh HOLD':>10}{'drift/anno':>13}{'maxDD':>9}") + for nm, s in (("senza funding", a0), ("CON funding", a1)): + r = np.asarray(s.dropna().values, float) + print(f" {nm:<22}{al._sh(s):>+10.3f}{al._sh(s[s.index>=HOLDOUT]):>+10.3f}" + f"{r.mean()*365.25:>12.2%}{100*al._dd_ret(s):>8.1f}%") + r0 = np.asarray(a0.dropna().values, float).mean() * 365.25 + r1 = np.asarray(a1.dropna().values, float).mean() * 365.25 + print(f" -> il funding costa a PREVDAY {r0-r1:.2%}/anno di drift e " + f"{al._sh(a0)-al._sh(a1):+.3f} di Sharpe FULL") + out["d_drift"] = r0 - r1 + return out + + +# ============================================================================================= +# 9) `weights_tilt_null` — il gate del progetto per ogni cambio di pesi +# ============================================================================================= +def sezione9(rate: dict) -> dict: + sez("9) `weights_tilt_null` — e la colonna che conta non e' il PASS") + if not B3RD_CACHE.exists(): + print(" cache §50 assente -> sezione SALTATA") + return {} + S = pickle.loads(B3RD_CACHE.read_bytes()) + cols = {"TP01": S["TP01"][0], "SKH01": S["SKH01"][0], "PREVDAY": prevday_daily_off(0, rate)} + k_seen = 7 * 4 + len(WEIGHTS_TESTED) # §50: 7 candidati x 4 pesi; qui: 4 pesi + print(f" k_seen dichiarato = {k_seen} (le {7*4} configurazioni guardate sull'hold-out in §50") + print(f" piu' le {len(WEIGHTS_TESTED)} di oggi: i pesi gia' visti si CONTANO, non si azzerano)") + w_cur = {"TP01": W_TP, "SKH01": W_SKH, "PREVDAY": 0.0} + print(f"\n {'peso':>6}{'d_insample':>13}{'d_hold':>10}{'d_full':>10}{'pctl_hold':>11}" + f"{'best-of-k':>11}{'rnd>cur':>10}{'GATE':>8}") + res = {} + for w in WEIGHTS_TESTED: + wp = {"TP01": W_TP * (1 - w), "SKH01": W_SKH * (1 - w), "PREVDAY": w} + r = weights_tilt_null(cols, w_cur, wp, floor=0.03, n=300, k_seen=k_seen) + res[w] = r + print(f" {w:>5.0%}{r['delta_insample']:>+13.4f}{r['delta_hold']:>+10.4f}" + f"{r['delta_full']:>+10.4f}{r['pctl_hold']:>10.1f}°{r['bestofk_pctl']:>10.1f}°" + f"{r['frac_random_beat_hold']:>10.2f}{('PASS' if r['gate_pass'] else 'FAIL'):>8}") + fr = res[W_PROPOSTO]["frac_random_beat_hold"] + print(f"\n ⚠️ `rnd>cur` = {fr:.2f}: e' la frazione di tilt CASUALI a 3 sleeve che battono il") + print(" libro attuale sull'hold-out. Dove vale ~0,9 il claim «PREVDAY migliora") + print(" l'hold-out» e' GENERICO: lo fa quasi qualunque terzo sleeve, e il gate passa") + print(" proprio perche' la proposta NON e' eccezionale fra i tilt casuali.") + print(" `weights_tilt_null` e' NECESSARIO, non sufficiente (sta nel suo docstring).") + return res + + +# ============================================================================================= +# 10) IL GATE PRE-REGISTRATO +# ============================================================================================= +def sezione10(S4: dict, S5: dict, S6: dict, S7: dict, S8: dict, S9: dict, + M: dict, MDE: dict) -> None: + sez("10) IL GATE PRE-REGISTRATO — data, soglie dichiarate PRIMA, veto, kill") + inizio = pd.Timestamp(M["st"]["start_ts"], unit="ms", tz="UTC") + OGGI = pd.Timestamp.now("UTC").date() + d1 = pd.Timestamp("2027-06-21", tz="UTC") # 1 anno di forward dall'inception + ndecl = len(S4["sel"]["all_full_sharpe"]) + print(f" Inception del monitor: {inizio.date()}. Oggi: {OGGI}.") + print(f" Finestra forward attuale: {M['span_h']/24:.0f} giorni = " + f"{M['span_h']/24/365.25:.2f} anni (sez. 3: SE(Sharpe) ~ " + f"{_se_sharpe(MDE['MODELED']['sh'], MDE['MODELED']['yrs']):.1f}).") + + W = 86 + + def bx(t: str = "") -> None: + print(" |" + t.ljust(W) + "|") + + print(" +" + "-" * W + "+") + bx(" GATE PREVDAY-01".ljust(W)) + bx() + bx(f" DATA DI DECISIONE: {d1.date()}") + bx(" (= 1 anno esatto di forward dall'inception. La data NON e' scelta perche' a 1") + bx(" anno la finestra diventi informativa — la sez. 3 misura che NON lo diventa —") + bx(" ma perche' oltre quell'orizzonte tenere un lead senza decidere e' esso stesso") + bx(" una decisione. La prova che conta e' gia' tutta OGGI e non migliora aspettando.)") + bx() + bx(" SI PROMUOVE a sleeve del LIBRO LIVE al peso 15% solo se TUTTE:") + bx(f" (a) deflated-Sharpe sulla famiglia DICHIARATA (N={ndecl}) >= {GATE_DSR_MIN}, con la") + bx(" griglia ri-dichiarata PRIMA di guardare e la sensibilita' alla PARTIZIONE") + bx(" pubblicata accanto (sez. 5): se cambia la partizione, cambia il verdetto.") + bx(" (b) la cella scelta IN-SAMPLE-ONLY coincide con quella CONGELATA nel monitor,") + bx(" oppure il monitor viene ri-congelato su quella al buio e il forward") + bx(" RIPARTE DA ZERO da quel giorno (non si eredita il track di un'altra cella).") + bx(f" (c) delta di libro APPAIATO sullo spazio d'ancora CONGIUNTO (TP01 x{len(TP_HOURS)} ·") + bx(f" SKH01 x{len(SKH_OFFSETS)} · PREVDAY x{len(PV_BOUNDARY)}) > {GATE_ANCHOR_DELTA_MIN:+.2f} di dShFULL al peso 15%,") + bx(f" positivo in >= {GATE_ANCHOR_FRAC_MIN:.0%} delle estrazioni, col FUNDING dentro.") + bx(" (d) `weights_tilt_null` gate_pass=True al peso proposto.") + bx(" (e) `marginal_vs_tp01` = ADDS, robust_oos, non-hedge SULLA CELLA AL BUIO.") + bx(" (f) `day_boundary_robust` != ARTIFACT-RISK sulla cella al buio.") + bx(" (g) Sharpe forward > 0 — soglia DEBOLE DI PROPOSITO: a 1 anno SE(Sharpe)~1,0 e") + bx(" una soglia alta sarebbe finta precisione (stessa scelta di DVOLSPREAD).") + bx() + bx(" VETO D'INTEGRITA' (si valuta PRIMA del resto e BLOCCA la decisione):") + bx(f" quota di barre forward RICOSTRUIBILI dal feed certificato >= {GATE_INTEGRITY_MIN:.0%} (sez. 2b),") + bx(" e i minuti di mercato NON registrati per giorno non devono CRESCERE rispetto a") + bx(" oggi. Sotto la soglia il gate SI ESTENDE, non si decide: una serie che non si") + bx(" ricostruisce non e' evidenza ne' a favore ne' contro.") + bx() + bx(f" KILL: Sharpe forward < {KILL_SHARPE:+.2f} su >= 180 giorni di barre ATTIVE (non di") + bx(" calendario). A quell'orizzonte la bassa potenza lavora nel verso giusto: si") + bx(" uccide solo cio' che e' molto negativo. Al kill il lead si chiude e la famiglia") + bx(" NON si ri-ottimizza (riaprirla dopo aver visto il forward e' selezione-") + bx(" sull'hold-out di secondo ordine).") + bx() + bx(" COSA NON SPOSTA IL GATE: un forward positivo. La sez. 3 misura che a 1 anno un") + bx(" Sharpe forward di +2 e uno di 0 non sono distinguibili. Il forward serve a") + bx(" UCCIDERE, non a promuovere.") + print(" +" + "-" * W + "+") + + sub("STATO OGGI delle condizioni — e' una FOTOGRAFIA, non anticipa la decisione") + W_ = W_PROPOSTO + rows = [ + ("(a) DSR famiglia dichiarata", f"{S4['dsr']:.3f} (N={ndecl})", + S4["dsr"] >= GATE_DSR_MIN), + ("(a') DSR con sr0 di SCREEN", f"{S5['dsr_screen']:.3f}", + S5["dsr_screen"] >= GATE_DSR_MIN), + ("(a'') DSR della cella CONGELATA", f"{S4['dsr_frozen']:.3f} (non e' la cella di (a))", + S4["dsr_frozen"] >= GATE_DSR_MIN), + ("(b) cella al buio == congelata", "SI" if S4["uguale"] else + f"NO (la congelata e' {S4['rank_frozen']}/{len(S4['sel']['rows'])} in-sample)", + S4["uguale"]), + ("(c) delta libro appaiato de-luckato", + (f"{S7[W_]['sh']:+.4f} FULL, >0 nel {S7[W_]['sh_pos']:.0%}") if S7 else "n/d", + bool(S7 and S7[W_]["sh"] > GATE_ANCHOR_DELTA_MIN + and S7[W_]["sh_pos"] >= GATE_ANCHOR_FRAC_MIN)), + ("(d) weights_tilt_null", ("PASS" if S9 and S9[W_]["gate_pass"] else "FAIL") + + (f" (rnd>cur {S9[W_]['frac_random_beat_hold']:.2f})" if S9 else ""), + bool(S9 and S9[W_]["gate_pass"])), + ("(e) marginal cella al buio", S4["sm"]["marginal_verdict"], + S4["sm"]["marginal_verdict"] == "ADDS" and S4["sm"]["earns_slot"]), + ("(f) day_boundary_robust", f"{S6['req']['verdict']} (spread {S6['req']['spread']:.3f})", + S6["req"]["verdict"] != "ARTIFACT-RISK"), + ("(g) Sharpe forward > 0", f"{MDE['MODELED']['sh']:+.2f} (t={MDE['MODELED']['t']:.2f}, " + f"p={MDE['MODELED']['p']:.2f})", MDE["MODELED"]["sh"] > 0), + ("VETO integrita'", f"{M['frac_ok']:.1%} barre ricostruibili", + M["frac_ok"] >= GATE_INTEGRITY_MIN), + ] + for nm, val, ok in rows: + print(f" {'OK ' if ok else 'NO '} {nm:<38}{val}") + npass = sum(1 for _, _, o in rows if o) + print(f"\n {npass}/{len(rows)} condizioni soddisfatte OGGI.") + print(f" Il funding costa a PREVDAY {S8['sleeve_drag']:.2%}/anno di sleeve = " + f"{W_*S8['sleeve_drag']:.2%}/anno di libro al peso proposto (gia' dentro (c)).") + + +# ============================================================================================= +def load_rate() -> dict: + """Serie oraria del funding per asset (LINEARE dove esiste, INVERSE prima come proxy + DICHIARATO). Usa la cache parquet di r0822d_funding; --no-net la usa e basta.""" + F = {} + for instr, (start, _a, _k) in FU.INSTR.items(): + d = FU.fetch_funding(instr, start).copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + F[instr] = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + return FU.build_rate(F) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--draws", type=int, default=300) + ap.add_argument("--no-net", action="store_true") + ap.add_argument("--fast", action="store_true") + args = ap.parse_args() + SCRATCH.mkdir(parents=True, exist_ok=True) + + print("=" * 97) + print(" §54 PREVDAY-GATE — il lead PREVDAY ha un gate pre-registrato? (2026-08-23)") + print(" NESSUNA proposta di cambio al libro live. Sola lettura sui file di produzione.") + print("=" * 97) + + grid = [dict(anchor=a, k=k, short=s, mh=m) + for a in G_ANCHOR for k in G_K for s in G_SHORT for m in G_MH] + tfs = G_TF + if args.fast: + grid = [g for g in grid if g["anchor"] in (1, 2) and g["mh"] == 0] + tfs = ("1h",) + print(" ⚠️ --fast: griglia RIDOTTA, i conteggi NON sono quelli dichiarati.") + + F = sezione0_1(grid, tfs) + M = sezione2(args) + MDE = sezione3(M) + print(f"\n {clock()} carico il funding ({'cache' if args.no_net else 'cache + incrementale'})...") + if args.no_net: + FU.USE_NET = False + rate = load_rate() + print(f" {clock()} funding pronto: " + " ".join( + f"{a} {rate[a]['series'].index.min().date()}->{rate[a]['series'].index.max().date()} " + f"(switch lineare {rate[a]['switch'].date()})" for a in ASSETS)) + + S4 = sezione4(grid, tfs) + S5 = sezione5(S4, F) + S6 = sezione6() + S7 = sezione7(rate, args.draws) + S8 = sezione8(rate) + S9 = sezione9(rate) + sezione10(S4, S5, S6, S7, S8, S9, M, MDE) + + sez("ATTESA A PRIORI vs ESITO — comprese quelle che ho sbagliato") + ch = S4["chosen"]["params"] + esiti = [ + ("A1 cella al buio != congelata", + "SBAGLIATA (coincidono)" if S4["uguale"] else "CONFERMATA nell'esito", + "" if S4["uguale"] else + f"⚠️ ma SBAGLIATA NEL MECCANISMO: prevedevo un k piu' piccolo; k resta {ch['k']:.2f} " + f"e a cambiare sono ALLOW_SHORT ({FROZEN['short']}->{ch['short']}) e il TF " + f"(1h->{S4['chosen']['tf']})"), + ("A2 il DSR si ribalta col conteggio", + f"REFUTATA (fam. {S4['dsr']:.3f} / screen {S5['dsr_screen']:.3f} / N=560 ancora PASS)", + f"⚠️ la leva non e' N ma la DISPERSIONE: servirebbe sd {S5['sd_soglia']:.2f} contro " + f"{S5['allsr'].std(ddof=1):.2f} misurata"), + ("A3 boundary robusto, banda larga, 0h fortunata", + f"CONFERMATA ({S6['req']['verdict']}, spread {S6['req']['spread']:.3f})", + f"canonica al 96o pctl delle 24: uplift +0,328 contro mediana +0,190"), + ("A4 il delta di §50 e' una sovrastima", + (f"CONFERMATA ({S7[W_PROPOSTO]['sh_can']:+.3f} -> {S7[W_PROPOSTO]['sh']:+.3f} FULL, " + f"{S7[W_PROPOSTO]['shh_can']:+.3f} -> {S7[W_PROPOSTO]['shh']:+.3f} HOLD)") if S7 else "n/d", + "il segno regge al 100% delle estrazioni; e' la taglia a dimezzarsi"), + ("A5 drag di funding 0-2%/anno", f"CONFERMATA ({S8['sleeve_drag']:.2%}/anno)", + "meccanismo NON come previsto: la gamba short incassa solo 0,26-0,46x " + "l'incondizionato — non compensa"), + ("A6 frac_random_beat_hold alto", + f"CONFERMATA ({S9[W_PROPOSTO]['frac_random_beat_hold']:.2f})" if S9 else "n/d", + "'migliora l'hold-out' e' un claim generico su questo libro"), + ("A7 la finestra forward non ha potenza", + f"CONFERMATA (t={MDE['MODELED']['t_naive']:.2f} naive / {MDE['MODELED']['t']:.2f} Lo)", + f"e il +2,04 pubblicato e' su lente ORARIA: sulla lente del progetto e' " + f"{MDE['daily_sharpe']:+.2f}"), + ] + for nm, v, nota in esiti: + print(f" {nm:<42}{v}") + if nota: + print(f" {'':<42}{nota}") + print(f"\n {clock()} fine.") + + +if __name__ == "__main__": + main()