aaddd2c93c
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
1208 lines
71 KiB
Python
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()
|