Files
Adriano Dal Pastro aaddd2c93c research(prevday): §54 GATE PREVDAY-01 scritto — il lead piu' forte gira la cella SBAGLIATA
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 02:55:39 +00:00

1208 lines
71 KiB
Python

#!/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} "
f"Sh HOLD {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()