From ebc8cf7d7c2cb08279709562f1c348a53e67be6f Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 22:02:26 +0000 Subject: [PATCH] =?UTF-8?q?research(wave-0822):=20GATE=20PROP-01=20gamba?= =?UTF-8?q?=20(b)=20PASS=2023/23=20=E2=80=94=20e=20la=20distorsione=20su?= =?UTF-8?q?=20SKH01=20dichiarata=20due=20volte=20e'=20refutata;=20P(50/g)?= =?UTF-8?q?=207,8%=20->=204,4%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/RESULTS-0822.md | 57 ++ scripts/research/r0822c_skh_anchor_prop.py | 604 +++++++++++++++++++++ 2 files changed, 661 insertions(+) create mode 100644 scripts/research/r0822c_skh_anchor_prop.py diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 4263a1b..34a02ea 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -697,6 +697,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 29 | PROP-RECAL | J rifatto con XS01 dalla storia lunga: il canale funded si apre? | **LEAD ridimensionato ~5x** — 42% → **7,8%** | | 30 | LISTINO | `GATE PROP-01` gamba (a): la firm lista >=10 delle 13 gambe con short? | **PASS 13/13** — verificato sul venue, non sul sito | | 31 | GATE-RECON | cosa direbbero i 3 gate pre-registrati se le serie forward fossero riparate | **1 gate su 3 ribaltato** — e la premessa e' CONFERMATA | +| 32 | SKH-ANCHOR-PROP | `GATE PROP-01` gamba (b), datata 31/10: girata oggi | **PASS 23/23** — e il numero scende ancora, a **4,4%** | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | | 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** — filone chiuso definitivamente | | 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | @@ -1431,3 +1432,59 @@ riparando oggi, il 27/09 si leggerebbe una finestra **piu' corta ma VERA** (SE(S ⚠️ `implausible_sharpe` **girato per la prima volta sulle serie forward**: chiede n≥30, quindi **2 serie su 3 non sono giudicabili**, e su statarb dice "ok" **in entrambe le lenti** — **il gate non vede un segno ribaltato**. + +--- + +## 32 — SKH-ANCHOR-PROP (`GATE PROP-01` gamba (b), chiusa oggi invece che il 31/10) + +`scripts/research/r0822c_skh_anchor_prop.py` (391 s, nessuna rete). **5.127 celle**, contate al +rialzo, con **griglia congiunta PIENA 23 offset × 10 fasi = 230 configurazioni** × 8 leve — **non** un +campionamento e **non** una somma di marginali (lezione `r0726_loo_deluck`: *la somma delle fortune +marginali non e' la mediana congiunta*). + +📌 **La data non aspettava un dato: aspettava che qualcuno misurasse il costo.** Il gate descriveva il +lavoro come *"rifare `pc._hourly_legs` ai 23 offset"*, che costa **0,6 s per offset**. I "~9 min per +asset×offset" sono il costo di `r0726_skh_sigcache` — **una computazione che questa misura non usa**. +*Secondo gate d'attesa eliminato oggi dopo quello di XS01-OOS, e per la stessa ragione: la stima del +costo non era stata fatta.* + +**`GATE PROP-01 GAMBA (b): PASS`** — delta appaiato mediano sui 23 offset **+0,1162**, banda p10-p90 +**[+0,087, +0,152]**, **23/23 offset positivi**, **230/230 celle congiunte positive** (soglia +pre-registrata +0,05). Regione: 38/38/25 +0,116 · **38/25/38 +0,120** · 25/38/38 +0,109. +✅ **Riproduzione 7/7 esatta** dei numeri di PROP-RECAL all'ancora canonica; **bit-exact 3/3 a 0,0** +(gambe orarie contro `pc._hourly_legs()` su 65.219 ore; costruttore di libro contro `PR.rg` a due +pesi), piu' la guardia "confini HTF ⊂ LTF" a ogni offset e l'assert che **l'indice giornaliero e' +invariante all'offset** — senza il quale il bootstrap non sarebbe appaiato. + +🚨 **MA il numero operativo scende di nuovo:** l'offset canonico 0 e' al **91° pctl dei 23** e regala +**+0,059 sul delta** e **+0,112 sul livello J**. J del candidato: canonico 0,453 → **mediana d'ancora +~0,347**; maxDD mediano **−10,8%** contro −9,6% pubblicato. +**P(≥50 €/g) da €600 in 36 mesi = mediana 4,4% [banda d'ancora 2,5%–8,2%], P(zero) 35,3%** — contro +7,8% / 25,5% di stamattina. ✅ All'ancora canonica la macchina **ridice 7,7%**: il crollo e' **fortuna +d'ancora, non disaccordo fra implementazioni**. + +📌 **IL RISULTATO INTERPRETATIVO, piu' importante del PASS: la distorsione dichiarata due volte — +*"l'ottimo si appoggia a SKH01, lo sleeve con la fortuna d'ancora piu' grande da restituire"* — e' +MISURATA E REFUTATA nella sua forma forte.** A **`38/25/38` SKH01 sta al medesimo peso del LIVE +(25%)**, la sua ancora si cancella quasi esattamente nella differenza appaiata, e li' il delta e' **il +piu' alto dei tre (+0,120)** con la banda **piu' stretta** (ampiezza 0,032 contro 0,065 e 0,057 delle +due che portano SKH a 38%). **L'ampiezza della banda scala col differenziale di peso su SKH01** — come +l'agente aveva previsto **prima** di misurare. **Cio' che paga e' XS01, non un sovrappeso dello sleeve +fragile.** + +⚠️ **Due cose che il PASS non deve nascondere.** +1. **Il gate e' su una DIFFERENZA, il numero operativo e' un LIVELLO:** nella prima l'ancora si + cancella in parte, nel secondo **per niente** → passano insieme e si muovono in modo diverso + (delta −33%, P(≥50/g) −44%). *Un gate superato non e' un numero confermato.* +2. **Il surrogato del 22/08 e' finito vicino al vero per la CANCELLAZIONE DI DUE ASIMMETRIE:** + ricalcolato a peso fisso il taglio del drift da' **+0,076/+0,045** (sottostima il vero +0,116), ma + PROP-RECAL lo calcolava **ri-cercando l'argmax sui 10 pesi migliori** mentre il LIVE resta + inchiodato a 75/25 — sotto stress il candidato puo' scappare da SKH01 e il LIVE no → si gonfia a + +0,124/+0,113. **Il numero era giusto, il metodo no.** + +⚠️ **Sensibilita' dichiarata:** il de-luck ×0,89 **su questa dimensione e' un doppio conteggio** (fu +misurato proprio come mediana-ancore/canonica del drift). A ×1,00 il delta sale a **+0,148**, 23/23 → +**tenerlo e' conservativo**. +**Smentitore residuo, fuori dal perimetro della gamba (b):** l'ancora **giornaliera di TP01** (24 ore), +che questa misura **non tocca** e che resta canonica 00:00 UTC = **la piu' fortunata delle 24** per +l'hold-out di TP01. E' la **terza** dimensione, e l'agente la dichiara. diff --git a/scripts/research/r0822c_skh_anchor_prop.py b/scripts/research/r0822c_skh_anchor_prop.py new file mode 100644 index 0000000..c49752a --- /dev/null +++ b/scripts/research/r0822c_skh_anchor_prop.py @@ -0,0 +1,604 @@ +"""r0822c_skh_anchor_prop — LA GAMBA (b) DEL `GATE PROP-01`, GIRATA DAVVERO (2026-08-22, ondata 3). + +COSA CHIUDE +----------- +`r0822b_prop_recal` (RESULTS-0822 §29) ha registrato un gate a tre gambe e ne ha DATATA una al +**2026-10-31**: + + (b) BANDA D'ANCORA DI SKH01 SOTTO LA LENTE PROP — MISURA. Data: 2026-10-31. + Costo: rifare `pc._hourly_legs` ai 23 offset della griglia 230m/690m e rigirare §3. + Metrica: MEDIANA DELLE DIFFERENZE APPAIATE J(peso nella regione w_XS 25-38%) - + J(libro LIVE 75/25) sui 23 offset, finestra W-LONG. PASS se > +0.05. + Surrogato di oggi (taglio del drift SKH x0.50 / x0.33): +0.124 e +0.113. + Mi aspetto un PASS e lo dichiaro prima. + +E' l'unica distorsione che il progetto ha dichiarato DUE VOLTE (PROP-ALLOC §1 e PROP-RECAL §29) +e mai rimossa: *l'ottimo gira sull'ancora canonica e si appoggia a SKH01, che il progetto misura +come lo sleeve con la fortuna d'ancora piu' grande da restituire*. + +⚠️ **LA DATA ERA UN BUDGET, E IL BUDGET ERA ANCORATO ALLA COMPUTAZIONE SBAGLIATA.** +Il costo temuto ("~9 min per asset x offset") e' quello di `r0726_skh_sigcache`, che ricostruisce +il segnale intra-bin a OGNI osservazione oraria dentro ogni bin 230m (~82.000 righe per cella). +Questa misura non ne ha bisogno: serve `pc._hourly_legs` all'offset, cioe' resample + entries + +MTM per-trade, **misurato 0,6 s per offset** (entrambi gli asset). I 23 offset costano ~15 s. +La gamba (b) non aspettava un dato ne' una macchina: aspettava che qualcuno misurasse il costo. + +IL PROBLEMA VERO: LE ANCORE SONO DUE +------------------------------------ +SKH01 ha 23 offset di griglia (230m/690m, uno ogni 30m su [0,690)); XS01 ha 10 fasi del ciclo +H=10. PROP-RECAL ha girato la banda di FASE (a offset SKH canonico); PROP-ALLOC nessuna delle due. +La lezione registrata di `r0726_loo_deluck` e' che **la somma delle fortune marginali NON e' la +mediana congiunta** (sbagliava di +0,82 di Sharpe hold-out). + +**SCELTA DICHIARATA: griglia congiunta PIENA 23 x 10 = 230 configurazioni**, non un campionamento +e non una somma di marginali. E' possibile perche' il costo e' stato MISURATO prima di scegliere +(~0,06 s per coppia config-leva): con 23 offset x 10 fasi x 8 leve la spesa e' ~110 s. +**Perche' questa scelta NON e' quella che mi conviene:** la scelta comoda sarebbe fissare la fase +di XS01 a 0, che PROP-RECAL misura fortunata di +0,019 di J; il collasso per MEDIANA sulle fasi la +restituisce. Entrambe sono riportate, cosi' si vede da che parte taglia. + +CONVENZIONI (tutte ereditate, nessuna nuova) +-------------------------------------------- +* Statistica = **MEDIANA DELLE DIFFERENZE APPAIATE**, mai differenza delle mediane (26/07: con + quella correzione un verdetto si ribalto'). Ogni offset e' valutato con ENTRAMBE le + configurazioni (candidato e LIVE) alla STESSA ancora e sugli STESSI indici di bootstrap. +* Lente **ACCOPPIATA** ovunque (su una regola di DD a UN GIORNO la close-only e' cieca). +* Pesi FISSI, mai ri-ottimizzati per offset: ri-cercare l'argmax a ogni offset sarebbe un + max-of-23, cioe' esattamente il difetto sotto audit. La leva si', ma su ENTRAMBI i lati. +* de-luck sul drift x0,89 tenuto **per comparabilita' con PROP-RECAL**. ⚠️ Su questa dimensione + e' un DOPPIO CONTEGGIO conservativo: lo 0,89 fu misurato (26/07) proprio come rapporto + mediana-ancore / ancora-canonica del drift. §6 rigira la misura a fattore 1,00. +* Universo XS01 = **U13 Binance** (la versione piu' DEBOLE, conservativa), finestra **W-LONG + 2021+**, meccanismo congelato. Identici a PROP-RECAL. + +COSA QUESTA MISURA NON TOGLIE (dichiarato, non nascosto) +-------------------------------------------------------- +L'ancora giornaliera di **TP01** (24 ore possibili) resta canonica 00:00 UTC, che il progetto +misura come la migliore delle 24 per l'hold-out di TP01. E' una TERZA dimensione, fuori dal +perimetro della gamba (b), e la sua fortuna resta dentro questi numeri. + +ESITO (2026-08-22, corsa completa 391 s, 5.127 celle dichiarate) +---------------------------------------------------------------- +* RIPRODUZIONE all'ancora canonica **7/7 esatta** (J LIVE 0,281 · fase 0 0,472 · mediana 0,453 · + banda [0,431 0,488] · delta +0,175 · 10/10 fasi) e **3 controlli bit-exact a 0,0** (gambe orarie + a off=0 contro `pc._hourly_legs()` su 4 colonne; costruttore di libro contro `PR.rg` a due pesi). +* **GAMBA (b): PASS.** Mediana delle differenze appaiate sui 23 offset = **+0,1162** contro la + soglia pre-registrata +0,05; banda p10-p90 **[+0,087, +0,152]**; **23/23 offset positivi**, + **230/230 celle** dello spazio congiunto positive. La regione tiene tutta: 38/38/25 +0,116 · + 38/25/38 +0,120 · 25/38/38 +0,109. +* **La fortuna d'ancora c'e' ed e' quella attesa, ma non e' dove si temeva.** L'offset canonico 0 + sta al **91° pctl** dei 23 su tutte e tre le grandezze e regala **+0,059 sul delta**, +0,112 sul + livello J del candidato, +0,050 su quello del LIVE. Il delta e' MENO ancorato dei livelli perche' + il LIVE contiene SKH01 al 25% e la fortuna si cancella in parte nella differenza appaiata. +* 📌 **IL RISULTATO INTERPRETATIVO: il vantaggio NON e' un tilt su SKH01.** A `38/25/38` SKH01 sta + al **medesimo peso del LIVE (25%)**, quindi la sua ancora si cancella quasi esattamente — e li' + il delta e' il **piu' alto dei tre (+0,120)** con la banda **piu' stretta** (ampiezza 0,032 + contro 0,065 di W_A e 0,057 di W_C, che portano SKH a 38%). **L'ampiezza della banda scala col + DIFFERENZIALE di peso su SKH01**, esattamente come previsto prima di misurare. Cio' che paga e' + XS01, non un sovrappeso dello sleeve fragile. +* ⚠️ **Il numero OPERATIVO invece si dimezza, e per una ragione diversa dal gate.** P(>=50 EUR/g) + da EUR 600 in 36 mesi: mediana della banda d'ancora **4,4% [p10-p90 2,5%-8,2%]** contro il + **7,8%** pubblicato; P(zero) **35,3%** contro 25,5%. All'ancora canonica la macchina ridice + **7,7%** (= replica di PROP-RECAL): il crollo e' la fortuna d'ancora, non un disaccordo. + **Il gate e' su una DIFFERENZA (dove l'ancora si cancella in parte), il numero operativo e' un + LIVELLO (dove non si cancella affatto) — passano insieme e si muovono in modo diverso.** +* ⚠️ **Il surrogato del 22/08 e' finito vicino al vero per la CANCELLAZIONE DI DUE ASIMMETRIE.** + Ricalcolato a peso FISSO il taglio del drift da' **+0,076 (x0,50)** e **+0,045 (x0,33)**, cioe' + SOTTOSTIMA il vero (+0,116); ma PROP-RECAL lo calcolava ri-cercando l'argmax sui 10 pesi + migliori mentre il LIVE resta inchiodato a 75/25 — sotto stress il candidato puo' scappare da + SKH01 e il LIVE no -> il surrogato si gonfia a +0,124/+0,113. **Due errori in versi opposti che + quasi si annullano: il numero era giusto, il metodo no.** +* Sensibilita' dichiarata: a de-luck **x1,00** (senza il doppio conteggio dell'ancora) il delta + sale a **+0,148**, 23/23 positivi -> tenere lo 0,89 e' conservativo su questa dimensione. +* Banda del maxDD del candidato sui 23 offset: **-8,5% ... -13,0%**, mediana **-10,8%** (il + pubblicato a peso 25% era -9,6%): su un conto a barriera la coda peggiora quanto il livello. +* **RESTA FUORI, dichiarato:** l'ancora giornaliera di **TP01** (24 ore) e' una TERZA dimensione, + fuori dal perimetro della gamba (b), e la sua fortuna e' ancora dentro questi numeri. + +Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822c_skh_anchor_prop.py` +Rete: NESSUNA (parquet certificati + cache Binance di r0822b_xs01_oos). +""" +from __future__ import annotations + +import sys +import time +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +import r0702_anchor_skh01 as AN # noqa: E402 resample con fase spostata (02/07) +import r0724_goal50_intraday_mc as mc # noqa: E402 recon orario per-sleeve (24/07) +import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07) +import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti +import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08) +import r0822b_prop_recal as PR # noqa: E402 IL file di cui giro la gamba (b) +import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance +from src.data.downloader import load_data # noqa: E402 +from src.strategies.skyhook import HTF_MIN, LTF_MIN # noqa: E402 + +ASSETS = ("BTC", "ETH") +OFFSETS = tuple(range(0, 690, 30)) # 23 offset a priori, identici a r0702 (0 = canonico) +PHASES = tuple(range(10)) # 10 fasi del ciclo H=10 di XS01 +PHASES_SUB = (0, 3, 7) # sottocampione a priori per la scala di conti +LEVS = PR.LEVS +N_PATHS = PA.N_PATHS +EV_H = FU_H = 365 +SEED_EV, SEED_FU = 8220, 8221 +HYRO = "HYRO" +GATE_B = 0.05 # soglia PRE-REGISTRATA da PROP-RECAL + +# pesi FISSI che coprono la regione robusta w_XS in [25%, 38%] (griglia simplesso a passo 1/8) +W_LIVE = {"TP01": 0.75, "SKH01": 0.25} +W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} # centro dichiarato da PROP-RECAL (headline) +W_B = {"TP01": 0.38, "SKH01": 0.25, "XS01": 0.38} # w_XS all'estremo alto, TP-pesante +W_C = {"TP01": 0.25, "SKH01": 0.38, "XS01": 0.38} # w_XS all'estremo alto, SKH-pesante + +# numeri PUBBLICATI da riprodurre prima di produrne di nuovi (RESULTS-0822 §29) +PUB = dict(j_med=0.453, j_ph0=0.472, j_p10=0.431, j_p90=0.488, j_live=0.281, + dmed=0.175, npos=10, surr_50=0.124, surr_33=0.113, p50g=0.078) + +TRIALS = 0 + + +def bump(n: int = 1) -> None: + global TRIALS + TRIALS += n + + +def hr(t: str = "") -> None: + print("\n" + "-" * 104) + if t: + print(f" {t}") + print("-" * 104) + + +# ==================================================================== 1. GAMBE ORARIE ALL'OFFSET + +_LEGS: dict[int, pd.DataFrame] = {} + + +def hourly_legs_off(off: int) -> pd.DataFrame: + """`pc._hourly_legs()` con la griglia 230m/690m di SKH01 spostata di `off` minuti. + + NON riscrive la macchineria: rimpiazza temporaneamente `mc.build_frames` con il resample + a fase spostata di `r0702_anchor_skh01` e richiama `mc.skh_hourly_contrib` — cosi' la + logica trade-per-trade (entry a close del bin, exit AL LIVELLO, SL prioritario, fee + nell'ora d'uscita) resta letteralmente quella del 24/07. La fedelta' della sostituzione e' + provata dal controllo bit-exact a off=0 (§1). + + La gamba TP01 NON viene toccata: l'ancora di TP01 e' un'ALTRA dimensione (24 ore giornaliere) + e la gamba (b) del gate riguarda i 23 offset di SKH01. + """ + if off in _LEGS: + return _LEGS[off] + tp_r = tp_w = sk_r = sk_w = None + orig = mc.build_frames + + def patched(df5: pd.DataFrame, _off: int = off): + ltf = AN.resample_off(df5, LTF_MIN, _off) + htf = AN.resample_off(df5, HTF_MIN, _off) + # confini HTF sottoinsieme dei LTF (690 = 3 x 230): la stessa guardia di r0702. + assert np.isin(htf["timestamp"].values[1:], ltf["timestamp"].values).all(), \ + f"confini HTF non sottoinsieme dei LTF (off={_off})" + return ltf, htf + + mc.build_frames = patched + try: + for a in ASSETS: + df = load_data(a, "1h") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + close = df["close"].values.astype(float) + low = df["low"].values.astype(float) + high = df["high"].values.astype(float) + prev = np.concatenate(([np.nan], close[:-1])) + r_close = close / prev - 1.0 + tpf = mc.tp01_daily_frame(a) + day_of = idx.floor("D") + w_tp = np.nan_to_num(tpf["w"].reindex(day_of).values) + fee_tp = np.nan_to_num(tpf["fee"].reindex(day_of).values) + first = np.concatenate(([True], day_of[1:] != day_of[:-1])) + r_wick = np.where(w_tp >= 0, low / prev - 1.0, high / prev - 1.0) + t_r = pd.Series(w_tp * r_close - np.where(first, fee_tp, 0.0), index=idx) + t_w = pd.Series(w_tp * r_wick - np.where(first, fee_tp, 0.0), index=idx) + sa, sb = mc.skh_hourly_contrib(a, idx, close, low, high) + s_r = pd.Series(sa / mc.W_SKH, index=idx) + s_w = pd.Series(sb / mc.W_SKH, index=idx) + tp_r = t_r if tp_r is None else tp_r.add(t_r, fill_value=np.nan) + tp_w = t_w if tp_w is None else tp_w.add(t_w, fill_value=np.nan) + sk_r = s_r if sk_r is None else sk_r.add(s_r, fill_value=np.nan) + sk_w = s_w if sk_w is None else sk_w.add(s_w, fill_value=np.nan) + finally: + mc.build_frames = orig + J = pd.concat({"tp_r": tp_r, "tp_w": tp_w, "skh_r": sk_r, "skh_w": sk_w}, axis=1).dropna() + _LEGS[off] = J + return J + + +_CRY: dict = {} + + +def crypto_off(off: int, w_tp: float, w_skh: float) -> pd.DataFrame: + """`pc.crypto_daily_tuples` con le gambe orarie all'offset `off` (minimo ESATTO sul path + orario condiviso: si compone l'ora per ora e si guarda il minimo, wick incluso).""" + key = (off, round(w_tp, 6), round(w_skh, 6)) + if key in _CRY: + return _CRY[key] + J = hourly_legs_off(off) + r_h = (w_tp * J["tp_r"] + w_skh * J["skh_r"]).values + w_h = (w_tp * J["tp_w"] + w_skh * J["skh_w"]).values + idx = J.index + days = idx.floor("D") + rows = [] + for day, pos in pd.Series(np.arange(len(idx)), index=days).groupby(level=0): + sl = pos.values + r = r_h[sl] + wk = w_h[sl] + cum = np.cumprod(1.0 + r) + cum_prev = np.concatenate(([1.0], cum[:-1])) + m = min(cum.min(), (cum_prev * (1.0 + wk)).min()) - 1.0 + rows.append((day, cum[-1] - 1.0, min(m, cum[-1] - 1.0))) + out = pd.DataFrame(rows, columns=["day", "R", "m"]).set_index("day") + _CRY[key] = out + return out + + +def rg_off(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, off: int) -> tuple: + """`PR.rg` con il sub-book crypto all'offset `off`. Validato bit-exact a off=0 in §1.""" + w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)) + w_xs = float(w.get("XS01", 0.0)) + R = np.zeros(len(idx)) + M = np.zeros(len(idx)) + if w_tp > 0 or w_sk > 0: + c = crypto_off(off, w_tp, w_sk).reindex(idx) + R += np.nan_to_num(c["R"].values) + M += np.nan_to_num(c["m"].values) + if w_xs > 0: + f = xs.reindex(idx) + R += w_xs * np.nan_to_num(f["R"].values) + M += w_xs * np.nan_to_num(f["m"].values) + return R, np.minimum(M, R) - R + + +def best_over_levers(R, G, ev, fu, factor: float = PA.DELUCK) -> tuple[float, float, np.ndarray]: + """(J, leva, vettore per-percorso) alla leva che massimizza J. La ricerca sulla leva e' + applicata SIMMETRICAMENTE al candidato e al LIVE (convenzione di PROP-RECAL).""" + jb, lb, vb = -1.0, None, None + for lv in LEVS: + o = PR.obj_rg(R, G, lv, ev, fu, factor=factor) + if o["J"] > jb: + jb, lb, vb = o["J"], lv, o["jvec"] + bump(len(LEVS)) + return jb, lb, vb + + +# ==================================================================== main +def main() -> None: + try: + sys.stdout.reconfigure(line_buffering=True) + except Exception: # pragma: no cover + pass + t0 = time.time() + + def tick(lab: str) -> None: + print(f" [{time.time()-t0:6.0f}s] {lab}") + + print("=" * 104) + print(" SKH-ANCHOR-PROP — la gamba (b) del `GATE PROP-01`, girata invece che aspettata") + print("=" * 104) + print(" DOMANDA: la mediana delle differenze appaiate J(regione w_XS 25-38%) - J(LIVE 75/25)") + print(f" sui 23 offset della griglia 230m/690m di SKH01, W-LONG, supera +{GATE_B:.2f}?") + print(" ASPETTATIVA REGISTRATA PRIMA DI MISURARE (mia, non ereditata): mi aspetto un PASS ma") + print(" con una taglia SENSIBILMENTE minore del surrogato (+0.124/+0.113), per un motivo") + print(" strutturale: il LIVE 75/25 contiene SKH01 al 25% e il candidato al 38%, quindi la") + print(" fortuna d'ancora si cancella in PARTE nella differenza appaiata — il residuo pesa") + print(" ~13 punti di peso, non 38. Se il delta uscisse >= al surrogato, il surrogato non era") + print(" conservativo e lo dico.") + + # ============================================ §0. RIPRODUZIONE + hr("§0. RIPRODUZIONE — la macchina deve dare i numeri di PROP-RECAL prima di darne di nuovi") + Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, PR.XS_UNIVERSE) + p13 = Cb[X.U13].dropna() + o13 = Ob.reindex(p13.index)[X.U13] + h13 = Hb.reindex(p13.index)[X.U13] + l13 = Lb.reindex(p13.index)[X.U13] + XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES} + bump(len(PHASES)) + cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index + base_idx = cry_idx.intersection(XSP[0].index) + first13 = XSP[0].index[np.flatnonzero(XSP[0]["R"].values != 0.0)[0]] + I_LONG = base_idx[base_idx >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))] + ev = PA.boot_idx(len(I_LONG), EV_H, N_PATHS, seed=SEED_EV) + fu = PA.boot_idx(len(I_LONG), FU_H, N_PATHS, seed=SEED_FU) + print(f" W-LONG: {len(I_LONG)} giorni {I_LONG[0].date()} -> {I_LONG[-1].date()} | " + f"de-luck x{PA.DELUCK} | {N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU} | firm {HYRO}") + + RL0, GL0 = PR.rg(W_LIVE, I_LONG, XSP[0]) + jL0, lvL0, vL0 = best_over_levers(RL0, GL0, ev, fu) + js0, ds0 = [], [] + for ph in PHASES: + Rp, Gp = PR.rg(W_A, I_LONG, XSP[ph]) + jb, _, vb = best_over_levers(Rp, Gp, ev, fu) + js0.append(jb) + ds0.append(float((vb - vL0).mean())) + print(f" {'grandezza':>42} {'mio':>9} {'PROP-RECAL':>11} esito") + checks = [("J libro LIVE 75/25 (ancora canonica)", jL0, PUB["j_live"]), + ("J candidato 38/38/25 fase 0", js0[0], PUB["j_ph0"]), + ("J candidato MEDIANA su 10 fasi", float(np.median(js0)), PUB["j_med"]), + ("banda di fase p10", float(np.percentile(js0, 10)), PUB["j_p10"]), + ("banda di fase p90", float(np.percentile(js0, 90)), PUB["j_p90"]), + ("delta appaiato MEDIANO (10 fasi)", float(np.median(ds0)), PUB["dmed"])] + ok = True + for lab, mine, pub in checks: + good = abs(mine - pub) <= 0.005 + ok &= good + print(f" {lab:>42} {mine:>9.3f} {pub:>11.3f} {'OK' if good else '<-- DIVERGE'}") + npos0 = sum(1 for d in ds0 if d > 0) + print(f" {'fasi con delta > 0':>42} {npos0:>9d} {PUB['npos']:>11d} " + f"{'OK' if npos0 == PUB['npos'] else '<-- DIVERGE'}") + ok &= npos0 == PUB["npos"] + print(f"\n >>> RIPRODUZIONE ALL'ANCORA CANONICA: " + f"{'OK — si procede' if ok else 'FALLITA — questo E il risultato'}") + if not ok: + print(" La macchina non riproduce PROP-RECAL: qualunque numero sugli offset sarebbe di") + print(" un'altra macchina. Mi fermo qui, come impone la disciplina di riproduzione.") + return + tick("§0 riproduzione") + + # ============================================ §1. LA MACCHINERIA ALL'OFFSET + hr("§1. LA MACCHINERIA ALL'OFFSET — e il controllo che prova che la sostituzione e' fedele") + t = time.time() + J0 = hourly_legs_off(0) + (Jref,) = pc._hourly_legs() + same_idx = J0.index.equals(Jref.index) + dmax = max(float(np.abs(J0[c].values - Jref[c].values).max()) for c in J0.columns) + print(f" gambe orarie a off=0 contro `pc._hourly_legs()`: n={len(J0)} indice identico=" + f"{same_idx} max|dif| su 4 colonne = {dmax:.2e} -> " + f"{'BIT-EXACT' if (same_idx and dmax == 0.0) else 'DIVERGE — STOP'}") + if not (same_idx and dmax == 0.0): + print(" La sostituzione di `build_frames` non riproduce il canonico: mi fermo.") + return + for wt in (W_A, W_LIVE): + Ra, Ga = rg_off(wt, I_LONG, XSP[0], 0) + Rb, Gb = PR.rg(wt, I_LONG, XSP[0]) + d1 = float(np.abs(Ra - Rb).max()) + d2 = float(np.abs(Ga - Gb).max()) + print(f" costruttore di libro a off=0 contro `PR.rg` ({PA.wkey(wt)}): " + f"max|dR| {d1:.2e} max|dgap| {d2:.2e} -> " + f"{'BIT-EXACT' if max(d1, d2) == 0.0 else 'DIVERGE'}") + if max(d1, d2) != 0.0: + return + print(f" costo misurato: {time.time()-t:.1f}s per la prima cella (import+cache inclusi)") + t = time.time() + for off in OFFSETS: + hourly_legs_off(off) + assert hourly_legs_off(off).index.equals(J0.index), \ + f"indice orario diverso a off={off}: il bootstrap non sarebbe appaiato" + print(f" 23 offset costruiti in {time.time()-t:.0f}s — l'indice giornaliero e' lo STESSO a") + print(" ogni offset, quindi gli indici di bootstrap sono condivisi e il confronto e' appaiato.") + print(" ⚠️ La data 2026-10-31 era un budget di calcolo ancorato al costo di `skh_sigcache`") + print(" (segnale intra-bin, ~9 min/cella), che questa misura non usa.") + n_tr = {} + for off in (0, 330, 660): + J = hourly_legs_off(off) + n_tr[off] = int((J["skh_r"].values != 0.0).sum()) + print(f" ore con contributo SKH01 non nullo: " + + " · ".join(f"off{o}: {v}" for o, v in n_tr.items()) + + " (griglie diverse, non una traslazione)") + tick("§1 macchineria") + + # ============================================ §2. LA GRIGLIA CONGIUNTA + hr("§2. LA GRIGLIA CONGIUNTA 23 OFFSET x 10 FASI — la gamba (b), headline W_A = 38/38/25") + print(" Per ogni offset: J del LIVE (che contiene SKH01 al 25% e quindi si muove anch'esso)") + print(" e J del candidato a ciascuna delle 10 fasi di XS01, alla leva che massimizza J.") + print(" Il delta e' appaiato sui MEDESIMI indici di bootstrap; il collasso sulle fasi e' la") + print(" MEDIANA (la stima onesta di PROP-RECAL), non la fase 0 (fortunata di +0.019).") + D = np.zeros((len(OFFSETS), len(PHASES))) + JC = np.zeros((len(OFFSETS), len(PHASES))) + JL = np.zeros(len(OFFSETS)) + LVL = np.zeros(len(OFFSETS)) + DDC = np.zeros((len(OFFSETS), len(PHASES))) + DDL = np.zeros(len(OFFSETS)) + t = time.time() + print(f"\n {'off':>4} {'J LIVE':>7} {'levL':>5} {'J cand fase0':>12} {'MEDIANA':>8} " + f"{'p10-p90':>15} {'delta MEDIANO':>13} {'fasi>0':>7} {'maxDD cand':>10}") + for i, off in enumerate(OFFSETS): + RL, GL = rg_off(W_LIVE, I_LONG, XSP[0], off) + jL, lvL, vL = best_over_levers(RL, GL, ev, fu) + JL[i], LVL[i] = jL, lvL + DDL[i] = PR.maxdd(PA.deluck(RL)) + for k, ph in enumerate(PHASES): + Rc, Gc = rg_off(W_A, I_LONG, XSP[ph], off) + jc, _, vc = best_over_levers(Rc, Gc, ev, fu) + JC[i, k] = jc + DDC[i, k] = PR.maxdd(PA.deluck(Rc)) + D[i, k] = float((vc - vL).mean()) + print(f" {off:>4} {jL:>7.3f} {lvL:>5.2f} {JC[i,0]:>12.3f} {np.median(JC[i]):>8.3f} " + f"[{np.percentile(JC[i],10):>6.3f},{np.percentile(JC[i],90):>6.3f}] " + f"{np.median(D[i]):>+13.3f} {int((D[i] > 0).sum()):>5}/10 " + f"{np.median(DDC[i])*100:>9.1f}%") + print(f" ({time.time()-t:.0f}s per 23 x (1 LIVE + 10 fasi) x 8 leve)") + + d_per_off = np.median(D, axis=1) # collasso onesto sulle fasi + d_gate = float(np.median(d_per_off)) # <-- IL NUMERO DELLA GAMBA (b) + d_ph0 = float(np.median(D[:, 0])) # la scelta comoda, per confronto + print(f"\n {'':>52}{'mediana':>9}{'p10':>9}{'p90':>9}{'>0':>8}") + print(f" {'delta appaiato per OFFSET (fasi collassate a mediana)':>52}" + f"{d_gate:>+9.3f}{np.percentile(d_per_off,10):>+9.3f}" + f"{np.percentile(d_per_off,90):>+9.3f}{int((d_per_off>0).sum()):>5}/23") + print(f" {'delta appaiato, spazio CONGIUNTO 230 celle':>52}" + f"{np.median(D):>+9.3f}{np.percentile(D,10):>+9.3f}" + f"{np.percentile(D,90):>+9.3f}{int((D>0).sum()):>4}/230") + print(f" {'delta a fase 0 fissa (la scelta COMODA)':>52}" + f"{d_ph0:>+9.3f}{np.percentile(D[:,0],10):>+9.3f}" + f"{np.percentile(D[:,0],90):>+9.3f}{int((D[:,0]>0).sum()):>5}/23") + print(f" {'ancora canonica (off 0, fase mediana) = PROP-RECAL':>52}" + f"{np.median(D[0]):>+9.3f}{'':>9}{'':>9}{int((D[0]>0).sum()):>5}/10") + print(f"\n Fortuna d'ancora di SKH01 SOTTO LA LENTE PROP (canonico - mediana):") + print(f" sul DELTA : {np.median(D[0]) - d_gate:+.3f} " + f"(l'offset 0 sta al {(d_per_off < d_per_off[0]).mean()*100:.0f}° pctl dei 23)") + print(f" sul LIVELLO J del candidato : {np.median(JC[0]) - np.median(np.median(JC,axis=1)):+.3f} " + f"(off 0 al {(np.median(JC,axis=1) < np.median(JC[0])).mean()*100:.0f}° pctl)") + print(f" sul LIVELLO J del LIVE : {JL[0] - np.median(JL):+.3f} " + f"(off 0 al {(JL < JL[0]).mean()*100:.0f}° pctl)") + print(" ⚠️ La fortuna si cancella in PARTE perche' il LIVE contiene SKH01 al 25%: il delta") + print(" e' meno ancorato dei livelli. E' la ragione per cui la statistica giusta e' il") + print(" DELTA APPAIATO e non il livello.") + tick("§2 griglia congiunta") + + # ============================================ §3. LA REGIONE, NON IL PUNTO + hr("§3. LA REGIONE w_XS 25-38% — il gate parla di una REGIONE, non di un punto") + print(" Tre pesi FISSI dichiarati che coprono la regione (nessuna ri-ottimizzazione per") + print(" offset: sarebbe un max-of-23, cioe' il difetto sotto audit). W_A gira sulle 10 fasi") + print(f" piene; i due pesi di sensibilita' sul sottocampione a priori {PHASES_SUB} (budget).") + print(f"\n {'peso':>12} {'w_XS':>5} {'J mediano':>10} {'delta MEDIANO':>13} " + f"{'p10':>8} {'p90':>8} {'offset>0':>9} {'celle>0':>9} {'gate':>6}") + reg = {} + for lab, wt, phs in (("38/38/25", W_A, PHASES), ("38/25/38", W_B, PHASES_SUB), + ("25/38/38", W_C, PHASES_SUB)): + if wt is W_A: + Dw = D + Jw = JC + else: + Dw = np.zeros((len(OFFSETS), len(phs))) + Jw = np.zeros((len(OFFSETS), len(phs))) + for i, off in enumerate(OFFSETS): + RL, GL = rg_off(W_LIVE, I_LONG, XSP[0], off) + _, _, vL = best_over_levers(RL, GL, ev, fu) + for k, ph in enumerate(phs): + Rc, Gc = rg_off(wt, I_LONG, XSP[ph], off) + jc, _, vc = best_over_levers(Rc, Gc, ev, fu) + Jw[i, k] = jc + Dw[i, k] = float((vc - vL).mean()) + dpo = np.median(Dw, axis=1) + dm = float(np.median(dpo)) + reg[lab] = dm + print(f" {lab:>12} {int(round(100*wt['XS01'])):>4}% {np.median(Jw):>10.3f} " + f"{dm:>+13.3f} {np.percentile(dpo,10):>+8.3f} {np.percentile(dpo,90):>+8.3f} " + f"{int((dpo>0).sum()):>7}/23 {int((Dw>0).sum()):>6}/{Dw.size} " + f"{'PASS' if dm > GATE_B else 'FAIL':>6}") + worst = min(reg.values()) + print(f"\n Peggiore della regione: {min(reg, key=reg.get)} con {worst:+.3f}; " + f"mediana dei tre {np.median(list(reg.values())):+.3f}") + tick("§3 regione") + + # ============================================ §4. SURROGATO vs MISURA + hr("§4. IL SURROGATO CONTRO LA MISURA — quanto valeva il taglio del drift") + print(" PROP-RECAL surrogava la banda d'ancora con un taglio del DRIFT di SKH01 (x0.50 e") + print(" x0.33) e leggeva +0.124 / +0.113. Qui lo stesso surrogato e' ricalcolato al peso") + print(" FISSO W_A e con lo stesso collasso di fase, cosi' il confronto e' appaiato.") + print(f"\n {'lente':>44} {'delta MEDIANO':>13}") + for lab, dmt in (("misura vera: 23 offset x 10 fasi", None), + ("surrogato SKH x0.50 (ancora canonica)", (("SKH01", 0.50),)), + ("surrogato SKH x0.33 (ancora canonica)", (("SKH01", 0.33),))): + if dmt is None: + print(f" {lab:>44} {d_gate:>+13.3f}") + continue + RLs, GLs = PR.rg(W_LIVE, I_LONG, XSP[0], dm=dmt) + _, _, vLs = best_over_levers(RLs, GLs, ev, fu) + dsx = [] + for ph in PHASES: + Rs, Gs = PR.rg(W_A, I_LONG, XSP[ph], dm=dmt) + _, _, vs = best_over_levers(Rs, Gs, ev, fu) + dsx.append(float((vs - vLs).mean())) + print(f" {lab:>44} {np.median(dsx):>+13.3f}") + print("\n Un surrogato che taglia il DRIFT non e' la stessa cosa di una griglia diversa:") + print(" cambiare l'ancora cambia QUALI trade esistono, non solo quanto rendono.") + tick("§4 surrogato") + + # ============================================ §5. IL NUMERO OPERATIVO + hr("§5. IL NUMERO OPERATIVO — P(>=50 EUR/g) da EUR 600 in 36 mesi, sulla banda d'ancora") + print(f" Sottocampione DICHIARATO: 23 offset x {len(PHASES_SUB)} fasi {PHASES_SUB} (uniformi a") + print(" priori sul ciclo H=10) = 69 simulazioni da 1500 percorsi. La scala di conti costa") + print(" ~1.7 s a simulazione: la griglia piena 23x10 costerebbe ~7 min in piu' e la mediana") + print(" di 69 estrazioni basta per una banda al punto percentuale.") + pc.DELUCK = PA.DELUCK + W3 = PA.CRYPTO_PROP + pt, pz, med_eur = [], [], [] + t = time.time() + if time.time() - t0 < 620: + for i, off in enumerate(OFFSETS): + for ph in PHASES_SUB: + Rc, Gc = rg_off(W_A, I_LONG, XSP[ph], off) + jc, lvc, _ = best_over_levers(Rc, Gc, ev, fu) + cfg = tuple(W_A.get(nm, 0.0) for nm in W3) + key = (cfg[0], cfg[1], cfg[2], "shared") + saved = pc._TUP_CACHE.pop(key, None) + pc._TUP_CACHE[key] = (Rc, Gc, I_LONG) + r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lvc, + seed=20260822, lens="coupled", start=str(I_LONG[0].date())) + pc._TUP_CACHE.pop(key, None) + if saved is not None: + pc._TUP_CACHE[key] = saved + pt.append(r["p_target"]); pz.append(r["p_zero"]); med_eur.append(r["med"]) + bump(len(OFFSETS) * len(PHASES_SUB)) + print(f"\n P(>=50 EUR/g) su {len(pt)} celle (offset x fase): mediana " + f"{np.median(pt):.1%} banda p10-p90 [{np.percentile(pt,10):.1%}, " + f"{np.percentile(pt,90):.1%}] min {min(pt):.1%} max {max(pt):.1%}") + print(f" P(zero) mediana {np.median(pz):.1%} | EUR/g mediana {np.median(med_eur):.2f}") + print(f" All'ancora CANONICA (off 0, fasi {PHASES_SUB}): " + f"{np.median(pt[:len(PHASES_SUB)]):.1%}") + print(f" PROP-RECAL pubblicava {PUB['p50g']:.1%} [7.2-9.1%] (ancora canonica, 10 fasi)") + print(f" ({time.time()-t:.0f}s)") + else: + print("\n NON GIRATO: budget di tempo esaurito prima di §5. Si dichiara, non si stima.") + pt = [] + tick("§5 numero operativo") + + # ============================================ §6. SENSIBILITA' AL DE-LUCK + hr("§6. SENSIBILITA' — il de-luck x0.89 su questa dimensione e' un doppio conteggio") + print(" Lo 0.89 fu MISURATO (26/07) come rapporto mediana-ancore / ancora-canonica del") + print(" drift: applicarlo mentre si gira la banda d'ancora conta due volte la stessa") + print(" correzione. E' tenuto per comparabilita' con PROP-RECAL; qui la stessa misura a") + print(" fattore 1.00 (nessun de-luck), sulle stesse 23 x 3 celle.") + d_nd = [] + for i, off in enumerate(OFFSETS): + RL, GL = rg_off(W_LIVE, I_LONG, XSP[0], off) + _, _, vL = best_over_levers(RL, GL, ev, fu, factor=1.0) + row = [] + for ph in PHASES_SUB: + Rc, Gc = rg_off(W_A, I_LONG, XSP[ph], off) + _, _, vc = best_over_levers(Rc, Gc, ev, fu, factor=1.0) + row.append(float((vc - vL).mean())) + d_nd.append(float(np.median(row))) + print(f"\n delta appaiato mediano de-luck x0.89 (headline) : {d_gate:+.3f}") + print(f" de-luck x1.00 (no double) : {np.median(d_nd):+.3f}" + f" ({int(sum(1 for d in d_nd if d > 0))}/23 > 0)") + print(" Il verso di questa differenza dice se la scelta conservativa lo era davvero.") + tick("§6 sensibilita'") + + # ============================================ §7. VERDETTO + hr("§7. VERDETTO SULLA GAMBA (b)") + passed = d_gate > GATE_B + print(f" Metrica pre-registrata: mediana delle differenze appaiate J(w_XS 25-38%) - J(LIVE)") + print(f" sui 23 offset, W-LONG. Soglia: > +{GATE_B:.2f}") + print(f"\n MISURA (headline W_A 38/38/25, fasi collassate a mediana): {d_gate:+.4f}") + print(f" banda p10-p90 sui 23 offset: [{np.percentile(d_per_off,10):+.3f}, " + f"{np.percentile(d_per_off,90):+.3f}] offset positivi: " + f"{int((d_per_off>0).sum())}/23 celle positive: {int((D>0).sum())}/230") + print(f" regione: peggiore {worst:+.3f} ({min(reg, key=reg.get)}), " + f"mediana dei tre pesi {np.median(list(reg.values())):+.3f}") + print(f"\n >>> GAMBA (b): {'PASS' if passed else 'FAIL'} " + f"({d_gate:+.4f} {'>' if passed else '<='} +{GATE_B:.2f})") + if passed: + print(" L'aspettativa registrata da PROP-RECAL (un PASS) e' confermata.") + else: + print(" L'aspettativa registrata da PROP-RECAL (un PASS) e' SMENTITA: il vantaggio") + print(" era in parte calendario.") + if pt: + print(f"\n COSA RESTA del risultato funded, ri-ancorato: P(>=50 EUR/g) mediana " + f"{np.median(pt):.1%} [banda d'ancora {np.percentile(pt,10):.1%}-" + f"{np.percentile(pt,90):.1%}] contro il {PUB['p50g']:.1%} pubblicato.") + print(f"\n RESTA FUORI (dichiarato): l'ancora giornaliera di TP01 (24 ore) — terza dimensione,") + print(" non nel perimetro della gamba (b), e la sua fortuna e' ancora dentro questi numeri.") + print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}") + print(f" fatto in {time.time()-t0:.0f}s") + print("=" * 104) + + +if __name__ == "__main__": + main()